mirror of
https://github.com/frappe/erpnext.git
synced 2026-03-19 06:52:12 +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:
|
||||
self.validate_for_repost()
|
||||
self.db_set("repost_required", self.needs_repost)
|
||||
self.repost_accounting_entries()
|
||||
|
||||
def on_cancel(self):
|
||||
# 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
|
||||
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"
|
||||
# Ledger reposted implicitly upon 'Update After Submit'
|
||||
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
|
||||
jv.load_from_db()
|
||||
self.expected_gle[0]["cost_center"] = "_Test Cost Center for BS Account - _TC"
|
||||
|
||||
@@ -808,6 +808,7 @@ class PurchaseInvoice(BuyingController):
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.db_set("repost_required", self.needs_repost)
|
||||
self.repost_accounting_entries()
|
||||
|
||||
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"
|
||||
|
||||
@@ -2005,10 +2005,9 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
check_gl_entries(self, pi.name, expected_gle, nowdate())
|
||||
|
||||
pi.items[0].expense_account = "Service - _TC"
|
||||
# Ledger reposted implicitly upon 'Update After Submit'
|
||||
pi.save()
|
||||
pi.load_from_db()
|
||||
self.assertTrue(pi.repost_required)
|
||||
pi.repost_accounting_entries()
|
||||
|
||||
expected_gle = [
|
||||
["Creditors - _TC", 0.0, 1000, nowdate()],
|
||||
|
||||
@@ -724,6 +724,7 @@ class SalesInvoice(SellingController):
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.db_set("repost_required", self.needs_repost)
|
||||
self.repost_accounting_entries()
|
||||
|
||||
def set_paid_amount(self):
|
||||
paid_amount = 0.0
|
||||
|
||||
@@ -2940,13 +2940,9 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
si.items[0].income_account = "Service - _TC"
|
||||
si.additional_discount_account = "_Test Account Sales - _TC"
|
||||
si.taxes[0].account_head = "TDS Payable - _TC"
|
||||
# Ledger reposted implicitly upon 'Update After Submit'
|
||||
si.save()
|
||||
|
||||
si.load_from_db()
|
||||
self.assertTrue(si.repost_required)
|
||||
|
||||
si.repost_accounting_entries()
|
||||
|
||||
expected_gle = [
|
||||
["_Test Account Sales - _TC", 22.0, 0.0, nowdate()],
|
||||
["Debtors - _TC", 88, 0.0, nowdate()],
|
||||
|
||||
@@ -123,19 +123,15 @@ def get_provisional_profit_loss(
|
||||
for period in period_list:
|
||||
key = period if consolidated else period.key
|
||||
total_assets = flt(asset[0].get(key))
|
||||
effective_liability = 0.00
|
||||
|
||||
if liability or equity:
|
||||
effective_liability = 0.0
|
||||
if liability:
|
||||
effective_liability += flt(liability[0].get(key))
|
||||
if equity:
|
||||
effective_liability += flt(equity[0].get(key))
|
||||
if liability:
|
||||
effective_liability += flt(liability[0].get(key))
|
||||
if equity:
|
||||
effective_liability += flt(equity[0].get(key))
|
||||
|
||||
provisional_profit_loss[key] = total_assets - effective_liability
|
||||
else:
|
||||
provisional_profit_loss[key] = total_assets
|
||||
|
||||
total_row[key] = provisional_profit_loss[key]
|
||||
provisional_profit_loss[key] = total_assets - effective_liability
|
||||
total_row[key] = provisional_profit_loss[key] + effective_liability
|
||||
|
||||
if provisional_profit_loss[key]:
|
||||
has_value = True
|
||||
|
||||
@@ -713,7 +713,8 @@ class GrossProfitGenerator:
|
||||
|
||||
def get_average_buying_rate(self, row, item_code):
|
||||
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(
|
||||
{
|
||||
"voucher_type": row.parenttype,
|
||||
@@ -727,9 +728,9 @@ class GrossProfitGenerator:
|
||||
args.update({"serial_and_batch_bundle": row.serial_and_batch_bundle})
|
||||
|
||||
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):
|
||||
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]
|
||||
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) {
|
||||
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);
|
||||
$(".form-message").text("Capitalize this asset to confirm");
|
||||
|
||||
@@ -511,6 +511,8 @@ frappe.ui.form.on("Asset", {
|
||||
frappe.call({
|
||||
args: {
|
||||
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",
|
||||
callback: function (r) {
|
||||
|
||||
@@ -75,8 +75,7 @@
|
||||
"purchase_amount",
|
||||
"default_finance_book",
|
||||
"depr_entry_posting_status",
|
||||
"amended_from",
|
||||
"capitalized_in"
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -222,7 +221,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!(doc.is_composite_asset && !doc.capitalized_in)",
|
||||
"depends_on": "eval:!doc.is_composite_asset",
|
||||
"fieldname": "gross_purchase_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Gross Purchase Amount",
|
||||
@@ -508,14 +507,6 @@
|
||||
"fieldtype": "Check",
|
||||
"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",
|
||||
"fieldname": "total_asset_cost",
|
||||
@@ -589,7 +580,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2024-05-21 13:46:21.066483",
|
||||
"modified": "2024-07-07 22:27:14.733839",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
|
||||
@@ -60,7 +60,6 @@ class Asset(AccountsController):
|
||||
available_for_use_date: DF.Date | None
|
||||
booked_fixed_asset: DF.Check
|
||||
calculate_depreciation: DF.Check
|
||||
capitalized_in: DF.Link | None
|
||||
company: DF.Link
|
||||
comprehensive_insurance: DF.Data | None
|
||||
cost_center: DF.Link | None
|
||||
@@ -162,7 +161,7 @@ class Asset(AccountsController):
|
||||
def on_cancel(self):
|
||||
self.validate_cancellation()
|
||||
self.cancel_movement_entries()
|
||||
self.cancel_capitalization()
|
||||
self.reload()
|
||||
self.delete_depreciation_entries()
|
||||
cancel_asset_depr_schedules(self)
|
||||
self.set_status()
|
||||
@@ -524,16 +523,6 @@ class Asset(AccountsController):
|
||||
movement = frappe.get_doc("Asset Movement", movement.get("name"))
|
||||
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):
|
||||
if self.calculate_depreciation:
|
||||
for row in self.get("finance_books"):
|
||||
@@ -872,10 +861,15 @@ def create_asset_repair(asset, asset_name):
|
||||
|
||||
|
||||
@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.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
|
||||
|
||||
|
||||
@@ -138,22 +138,10 @@ class AssetCapitalization(StockController):
|
||||
"Asset",
|
||||
"Asset Movement",
|
||||
)
|
||||
self.cancel_target_asset()
|
||||
self.update_stock_ledger()
|
||||
self.make_gl_entries()
|
||||
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):
|
||||
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"):
|
||||
frappe.throw(_("Consumed Asset Items is mandatory for Decapitalization"))
|
||||
|
||||
if not self.get("stock_items") and not self.get("asset_items"):
|
||||
frappe.throw(_("Consumed Stock Items or Consumed Asset Items is mandatory for Capitalization"))
|
||||
if not (self.get("stock_items") or self.get("asset_items") or self.get("service_items")):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Consumed Stock Items, Consumed Asset Items or Consumed Service Items is mandatory for Capitalization"
|
||||
)
|
||||
)
|
||||
|
||||
def validate_item(self, item):
|
||||
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.gross_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.asset_created_via_asset_capitalization = True
|
||||
asset_doc.insert()
|
||||
@@ -653,7 +644,6 @@ class AssetCapitalization(StockController):
|
||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||
asset_doc.gross_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.save()
|
||||
|
||||
|
||||
@@ -386,6 +386,56 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
self.assertFalse(get_actual_gle_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():
|
||||
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),
|
||||
"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(
|
||||
d, type_of_transaction=type_of_transaction
|
||||
)
|
||||
@@ -580,6 +582,14 @@ class BuyingController(SubcontractingController):
|
||||
(not cint(self.is_return) and self.docstatus == 2)
|
||||
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(
|
||||
d,
|
||||
{
|
||||
@@ -589,7 +599,7 @@ class BuyingController(SubcontractingController):
|
||||
"serial_and_batch_bundle": (
|
||||
self.get_package_for_target_warehouse(d, d.from_warehouse, "Inward")
|
||||
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):
|
||||
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"))
|
||||
|
||||
return find_variant(template, args, variant)
|
||||
@@ -197,7 +198,8 @@ def find_variant(template, args, variant_item_code=None):
|
||||
|
||||
|
||||
@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):
|
||||
args = json.loads(args)
|
||||
|
||||
@@ -211,13 +213,18 @@ def create_variant(item, args):
|
||||
|
||||
variant.set("attributes", variant_attributes)
|
||||
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)
|
||||
|
||||
return variant
|
||||
|
||||
|
||||
@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
|
||||
if isinstance(args, str):
|
||||
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"))
|
||||
return
|
||||
if total_variants < 10:
|
||||
return create_multiple_variants(item, args)
|
||||
return create_multiple_variants(item, args, use_template_image)
|
||||
else:
|
||||
frappe.enqueue(
|
||||
"erpnext.controllers.item_variant.create_multiple_variants",
|
||||
item=item,
|
||||
args=args,
|
||||
use_template_image=use_template_image,
|
||||
now=frappe.flags.in_test,
|
||||
)
|
||||
return "queued"
|
||||
|
||||
|
||||
def create_multiple_variants(item, args):
|
||||
def create_multiple_variants(item, args, use_template_image=False):
|
||||
count = 0
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
template_item = frappe.get_doc("Item", item)
|
||||
args_set = generate_keyed_value_combinations(args)
|
||||
|
||||
for attribute_values in args_set:
|
||||
if not get_variant(item, args=attribute_values):
|
||||
variant = create_variant(item, attribute_values)
|
||||
if use_template_image and template_item.image:
|
||||
variant.image = template_item.image
|
||||
variant.save()
|
||||
count += 1
|
||||
|
||||
|
||||
@@ -538,7 +538,9 @@ class SellingController(StockController):
|
||||
self.make_sl_entries(sl_entries)
|
||||
|
||||
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 self.docstatus == 1:
|
||||
serial_and_batch_bundle = self.make_package_for_transfer(
|
||||
|
||||
@@ -908,6 +908,7 @@ class SubcontractingController(StockController):
|
||||
item,
|
||||
{
|
||||
"item_code": item.rm_item_code,
|
||||
"incoming_rate": item.rate if self.is_return else 0,
|
||||
"warehouse": self.supplier_warehouse,
|
||||
"actual_qty": -1 * flt(item.consumed_qty, item.precision("consumed_qty")),
|
||||
"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.v15_0.delete_orphaned_asset_movement_item_records
|
||||
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.v14_0.enable_set_priority_for_pricing_rules #1
|
||||
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:
|
||||
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)
|
||||
|
||||
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)
|
||||
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):
|
||||
dn = frappe.new_doc("Delivery Note")
|
||||
|
||||
@@ -587,6 +587,14 @@ $.extend(erpnext.item, {
|
||||
me.multiple_variant_dialog = new frappe.ui.Dialog({
|
||||
title: __("Select Attribute Values"),
|
||||
fields: [
|
||||
frm.doc.image
|
||||
? {
|
||||
fieldtype: "Check",
|
||||
label: __("Create a variant with the template image."),
|
||||
fieldname: "use_template_image",
|
||||
default: 0,
|
||||
}
|
||||
: null,
|
||||
{
|
||||
fieldtype: "HTML",
|
||||
fieldname: "help",
|
||||
@@ -594,11 +602,14 @@ $.extend(erpnext.item, {
|
||||
${__("Select at least one value from each of the attributes.")}
|
||||
</label>`,
|
||||
},
|
||||
].concat(fields),
|
||||
]
|
||||
.concat(fields)
|
||||
.filter(Boolean),
|
||||
});
|
||||
|
||||
me.multiple_variant_dialog.set_primary_action(__("Create Variants"), () => {
|
||||
let selected_attributes = get_selected_attributes();
|
||||
let use_template_image = me.multiple_variant_dialog.get_value("use_template_image");
|
||||
|
||||
me.multiple_variant_dialog.hide();
|
||||
frappe.call({
|
||||
@@ -606,6 +617,7 @@ $.extend(erpnext.item, {
|
||||
args: {
|
||||
item: frm.doc.name,
|
||||
args: selected_attributes,
|
||||
use_template_image: use_template_image,
|
||||
},
|
||||
callback: function (r) {
|
||||
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({
|
||||
title: __("Create Variant"),
|
||||
fields: fields,
|
||||
@@ -761,6 +782,7 @@ $.extend(erpnext.item, {
|
||||
args: {
|
||||
item: frm.doc.name,
|
||||
args: d.get_values(),
|
||||
use_template_image: args.use_template_image,
|
||||
},
|
||||
callback: function (r) {
|
||||
var doclist = frappe.model.sync(r.message);
|
||||
|
||||
@@ -1174,3 +1174,34 @@ class TestPickList(FrappeTestCase):
|
||||
row.qty = row.qty + 10
|
||||
|
||||
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.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():
|
||||
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},
|
||||
"voucher_type": self.voucher_type,
|
||||
"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",
|
||||
},
|
||||
"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,
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"section_break_9",
|
||||
"over_delivery_receipt_allowance",
|
||||
"mr_qty_allowance",
|
||||
"over_picking_allowance",
|
||||
"column_break_121",
|
||||
"role_allowed_to_over_deliver_receive",
|
||||
"allow_negative_stock",
|
||||
@@ -446,6 +447,12 @@
|
||||
"fieldname": "do_not_use_batchwise_valuation",
|
||||
"fieldtype": "Check",
|
||||
"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",
|
||||
@@ -453,7 +460,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-07-04 12:45:09.811280",
|
||||
"modified": "2024-07-15 17:18:23.872161",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Settings",
|
||||
|
||||
@@ -47,6 +47,7 @@ class StockSettings(Document):
|
||||
mr_qty_allowance: DF.Float
|
||||
naming_series_prefix: DF.Data | None
|
||||
over_delivery_receipt_allowance: DF.Float
|
||||
over_picking_allowance: DF.Percent
|
||||
pick_serial_and_batch_based_on: DF.Literal["FIFO", "LIFO", "Expiry"]
|
||||
reorder_email_notify: DF.Check
|
||||
role_allowed_to_create_edit_back_dated_transactions: DF.Link | None
|
||||
|
||||
@@ -77,9 +77,8 @@ def get_columns(filters):
|
||||
},
|
||||
{
|
||||
"label": _("Party Type"),
|
||||
"fieldtype": "Link",
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "party_type",
|
||||
"options": "DocType",
|
||||
"width": 90,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -599,9 +599,15 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
|
||||
|
||||
timestamp_condition = ""
|
||||
if self.sle.posting_date and self.sle.posting_time:
|
||||
timestamp_condition = CombineDatetime(
|
||||
parent.posting_date, parent.posting_time
|
||||
) <= CombineDatetime(self.sle.posting_date, self.sle.posting_time)
|
||||
timestamp_condition = CombineDatetime(parent.posting_date, parent.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 = (
|
||||
frappe.qb.from_(parent)
|
||||
|
||||
@@ -200,7 +200,7 @@ def get_bin(item_code, warehouse):
|
||||
if not bin:
|
||||
bin_obj = _create_bin(item_code, warehouse)
|
||||
else:
|
||||
bin_obj = frappe.get_doc("Bin", bin, for_update=True)
|
||||
bin_obj = frappe.get_doc("Bin", bin)
|
||||
bin_obj.flags.ignore_permissions = True
|
||||
return bin_obj
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ frappe.ui.form.on("Subcontracting Order", {
|
||||
setup: (frm) => {
|
||||
frm.get_field("items").grid.cannot_add_rows = true;
|
||||
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"));
|
||||
|
||||
@@ -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) => {
|
||||
if (!frm.doc.transaction_date) {
|
||||
frm.set_value("transaction_date", frappe.datetime.get_today());
|
||||
@@ -116,6 +128,8 @@ frappe.ui.form.on("Subcontracting Order", {
|
||||
},
|
||||
|
||||
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.status == "Closed") {
|
||||
frm.add_custom_button(
|
||||
|
||||
@@ -28,6 +28,8 @@ frappe.ui.form.on("Subcontracting Receipt", {
|
||||
},
|
||||
|
||||
refresh: (frm) => {
|
||||
frappe.dynamic_link = { doc: frm.doc, fieldname: "supplier", doctype: "Supplier" };
|
||||
|
||||
if (frm.doc.docstatus === 1) {
|
||||
frm.add_custom_button(
|
||||
__("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", () => {
|
||||
return {
|
||||
filters: {
|
||||
|
||||
Reference in New Issue
Block a user