Merge pull request #42354 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
ruthra kumar
2024-07-17 10:48:30 +05:30
committed by GitHub
34 changed files with 481 additions and 97 deletions

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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()],

View File

@@ -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

View File

@@ -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()],

View File

@@ -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

View File

@@ -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")

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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",

View File

@@ -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

View File

@@ -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()

View File

@@ -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)

View File

@@ -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
),
},
)

View File

@@ -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

View File

@@ -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(

View File

@@ -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,

View File

@@ -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

View File

@@ -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)

View File

@@ -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):

View File

@@ -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")

View File

@@ -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);

View File

@@ -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)

View File

@@ -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

View File

@@ -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,
}
)

View File

@@ -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,

View File

@@ -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",

View File

@@ -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

View File

@@ -77,9 +77,8 @@ def get_columns(filters):
},
{
"label": _("Party Type"),
"fieldtype": "Link",
"fieldtype": "Data",
"fieldname": "party_type",
"options": "DocType",
"width": 90,
},
{

View File

@@ -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)

View File

@@ -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

View File

@@ -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(

View File

@@ -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: {