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

chore: release v15
This commit is contained in:
ruthra kumar
2024-10-04 08:35:53 +05:30
committed by GitHub
33 changed files with 665 additions and 218 deletions

View File

@@ -109,7 +109,7 @@ def get_api_endpoint(service_provider: str | None = None, use_http: bool = False
if service_provider == "exchangerate.host":
api = "api.exchangerate.host/convert"
elif service_provider == "frankfurter.app":
api = "frankfurter.app/{transaction_date}"
api = "api.frankfurter.app/{transaction_date}"
protocol = "https://"
if use_http:

View File

@@ -210,19 +210,31 @@ def get_linked_dunnings_as_per_state(sales_invoice, state):
@frappe.whitelist()
def get_dunning_letter_text(dunning_type, doc, language=None):
def get_dunning_letter_text(dunning_type: str, doc: str | dict, language: str | None = None) -> dict:
DOCTYPE = "Dunning Letter Text"
FIELDS = ["body_text", "closing_text", "language"]
if isinstance(doc, str):
doc = json.loads(doc)
if not language:
language = doc.get("language")
if language:
filters = {"parent": dunning_type, "language": language}
else:
filters = {"parent": dunning_type, "is_default_language": 1}
letter_text = frappe.db.get_value(
"Dunning Letter Text", filters, ["body_text", "closing_text", "language"], as_dict=1
)
if letter_text:
return {
"body_text": frappe.render_template(letter_text.body_text, doc),
"closing_text": frappe.render_template(letter_text.closing_text, doc),
"language": letter_text.language,
}
letter_text = frappe.db.get_value(
DOCTYPE, {"parent": dunning_type, "language": language}, FIELDS, as_dict=1
)
if not letter_text:
letter_text = frappe.db.get_value(
DOCTYPE, {"parent": dunning_type, "is_default_language": 1}, FIELDS, as_dict=1
)
if not letter_text:
return {}
return {
"body_text": frappe.render_template(letter_text.body_text, doc),
"closing_text": frappe.render_template(letter_text.closing_text, doc),
"language": letter_text.language,
}

View File

@@ -2292,6 +2292,24 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
def test_last_purchase_rate(self):
item = create_item("_Test Item For Last Purchase Rate from PI", is_stock_item=1)
pi1 = make_purchase_invoice(item_code=item.item_code, qty=10, rate=100)
item.reload()
self.assertEqual(item.last_purchase_rate, 100)
pi2 = make_purchase_invoice(item_code=item.item_code, qty=10, rate=200)
item.reload()
self.assertEqual(item.last_purchase_rate, 200)
pi2.cancel()
item.reload()
self.assertEqual(item.last_purchase_rate, 100)
pi1.cancel()
item.reload()
self.assertEqual(item.last_purchase_rate, 0)
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(

View File

@@ -3309,7 +3309,6 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
items_added_or_removed = False # updated to true if any new item is added or removed
any_conversion_factor_changed = False
sales_doctypes = ["Sales Order", "Sales Invoice", "Delivery Note", "Quotation"]
parent = frappe.get_doc(parent_doctype, parent_doctype_name)
check_doc_permissions(parent, "write")
@@ -3425,25 +3424,21 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
# if rate is greater than price_list_rate, set margin
# or set discount
child_item.discount_percentage = 0
if parent_doctype in sales_doctypes:
child_item.margin_type = "Amount"
child_item.margin_rate_or_amount = flt(
child_item.rate - child_item.price_list_rate,
child_item.precision("margin_rate_or_amount"),
)
child_item.rate_with_margin = child_item.rate
child_item.margin_type = "Amount"
child_item.margin_rate_or_amount = flt(
child_item.rate - child_item.price_list_rate,
child_item.precision("margin_rate_or_amount"),
)
child_item.rate_with_margin = child_item.rate
else:
child_item.discount_percentage = flt(
(1 - flt(child_item.rate) / flt(child_item.price_list_rate)) * 100.0,
child_item.precision("discount_percentage"),
)
child_item.discount_amount = flt(child_item.price_list_rate) - flt(child_item.rate)
if parent_doctype in sales_doctypes:
child_item.margin_type = ""
child_item.margin_rate_or_amount = 0
child_item.rate_with_margin = 0
child_item.margin_type = ""
child_item.margin_rate_or_amount = 0
child_item.rate_with_margin = 0
child_item.flags.ignore_validate_update_after_submit = True
if new_child_flag:

View File

@@ -702,9 +702,11 @@ class BuyingController(SubcontractingController):
if self.get("is_return"):
return
if self.doctype in ["Purchase Order", "Purchase Receipt"] and not frappe.db.get_single_value(
"Buying Settings", "disable_last_purchase_rate"
):
if self.doctype in [
"Purchase Order",
"Purchase Receipt",
"Purchase Invoice",
] and not frappe.db.get_single_value("Buying Settings", "disable_last_purchase_rate"):
update_last_purchase_rate(self, is_submit=0)
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:

View File

@@ -64,6 +64,18 @@ class StockController(AccountsController):
self.validate_internal_transfer()
self.validate_putaway_capacity()
def validate_items_exist(self):
if not self.get("items"):
return
items = [d.item_code for d in self.get("items")]
exists_items = frappe.get_all("Item", filters={"name": ("in", items)}, pluck="name")
non_exists_items = set(items) - set(exists_items)
if non_exists_items:
frappe.throw(_("Items {0} do not exist in the Item master.").format(", ".join(non_exists_items)))
def validate_duplicate_serial_and_batch_bundle(self, table_name):
if not self.get(table_name):
return

View File

@@ -40,7 +40,7 @@ class calculate_taxes_and_totals:
return items
def calculate(self):
if not len(self._items):
if not len(self.doc.items):
return
self.discount_amount_applied = False
@@ -95,7 +95,7 @@ class calculate_taxes_and_totals:
if self.doc.get("is_return") and self.doc.get("return_against"):
return
for item in self._items:
for item in self.doc.items:
if item.item_code and item.get("item_tax_template"):
item_doc = frappe.get_cached_doc("Item", item.item_code)
args = {
@@ -154,7 +154,7 @@ class calculate_taxes_and_totals:
return
if not self.discount_amount_applied:
for item in self._items:
for item in self.doc.items:
self.doc.round_floats_in(item)
if item.discount_percentage == 100:
@@ -258,7 +258,7 @@ class calculate_taxes_and_totals:
if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")):
return
for item in self._items:
for item in self.doc.items:
item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
cumulated_tax_fraction = 0
total_inclusive_tax_amount_per_qty = 0

View File

@@ -1531,6 +1531,197 @@ class TestWorkOrder(FrappeTestCase):
self.assertFalse(serial_nos)
def test_backflushed_batch_raw_materials_based_on_transferred_autosabb(self):
frappe.db.set_single_value(
"Manufacturing Settings",
"backflush_raw_materials_based_on",
"Material Transferred for Manufacture",
)
batch_item = "Test Batch MCC Keyboard"
fg_item = "Test FG Item with Batch Raw Materials"
ste_doc = test_stock_entry.make_stock_entry(
item_code=batch_item, target="Stores - _TC", qty=8, basic_rate=100, do_not_save=True
)
# Inward raw materials in Stores warehouse
ste_doc.submit()
ste_doc.reload()
batch_no = get_batch_from_bundle(ste_doc.items[0].serial_and_batch_bundle)
wo_doc = make_wo_order_test_record(production_item=fg_item, qty=4)
# action taken upon Start button:
transferred_ste_doc = frappe.get_doc(
make_stock_entry(wo_doc.name, "Material Transfer for Manufacture", 4)
)
transferred_ste_doc.submit()
transferred_ste_doc.reload()
self.assertTrue(transferred_ste_doc.items[0].serial_and_batch_bundle)
self.assertEqual(
get_batch_from_bundle(transferred_ste_doc.items[0].serial_and_batch_bundle), batch_no
)
self.assertEqual(transferred_ste_doc.items[0].qty, 4.0)
# Make additional consumption and link to WO
test_stock_entry.make_stock_entry(
item_code="Test Batch Battery Consumable",
target="Stores - _TC",
qty=8,
basic_rate=2.33,
)
consume_use_doc = test_stock_entry.make_stock_entry(
item_code="Test Batch Battery Consumable", # consumable not linked to BOM
source="Stores - _TC",
qty=4,
purpose="Material Consumption for Manufacture",
do_not_save=True,
)
consume_use_doc.work_order = wo_doc.name
consume_use_doc.fg_completed_qty = 4
consume_use_doc.submit()
consume_use_doc.reload()
manufacture_ste_doc = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 4))
mfr_items = [i.as_dict() for i in manufacture_ste_doc.items]
manufacture_ste_doc.submit()
manufacture_ste_doc.reload()
self.assertTrue(len(mfr_items), 2)
self.assertTrue(manufacture_ste_doc.items[0].serial_and_batch_bundle)
self.assertEqual(
get_batch_from_bundle(manufacture_ste_doc.items[0].serial_and_batch_bundle), batch_no
)
self.assertEqual(manufacture_ste_doc.items[0].qty, 4.0)
def test_backflushed_serial_no_raw_materials_based_on_transferred_autosabb(self):
frappe.db.set_single_value(
"Manufacturing Settings",
"backflush_raw_materials_based_on",
"Material Transferred for Manufacture",
)
sn_item = "Test Serial No BTT Headphone"
fg_item = "Test FG Item with Serial No Raw Materials"
ste_doc = test_stock_entry.make_stock_entry(
item_code=sn_item, target="Stores - _TC", qty=4, basic_rate=100, do_not_save=True
)
# Inward raw materials in Stores warehouse
ste_doc.submit()
ste_doc.reload()
serial_nos_list = sorted(get_serial_nos_from_bundle(ste_doc.items[0].serial_and_batch_bundle))
wo_doc = make_wo_order_test_record(production_item=fg_item, qty=4)
transferred_ste_doc = frappe.get_doc(
make_stock_entry(wo_doc.name, "Material Transfer for Manufacture", 4)
)
transferred_ste_doc.submit()
transferred_ste_doc.reload()
self.assertTrue(transferred_ste_doc.items[0].serial_and_batch_bundle)
self.assertEqual(
sorted(get_serial_nos_from_bundle(transferred_ste_doc.items[0].serial_and_batch_bundle)),
serial_nos_list,
)
self.assertEqual(transferred_ste_doc.items[0].qty, 4.0)
# Make additional consumption and link to WO
test_stock_entry.make_stock_entry(
item_code="Test Serial Battery Consumable",
target="Stores - _TC",
qty=8,
basic_rate=3.33,
)
consume_use_doc = test_stock_entry.make_stock_entry(
item_code="Test Serial Battery Consumable", # consumable not linked to BOM
source="Stores - _TC",
qty=4,
purpose="Material Consumption for Manufacture",
do_not_save=True,
)
consume_use_doc.work_order = wo_doc.name
consume_use_doc.fg_completed_qty = 4
consume_use_doc.submit()
consume_use_doc.reload()
manufacture_ste_doc = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 4))
mfr_items = [i.as_dict() for i in manufacture_ste_doc.items]
manufacture_ste_doc.submit()
manufacture_ste_doc.reload()
self.assertTrue(len(mfr_items), 2)
self.assertTrue(manufacture_ste_doc.items[0].serial_and_batch_bundle)
self.assertEqual(
sorted(get_serial_nos_from_bundle(manufacture_ste_doc.items[0].serial_and_batch_bundle)),
serial_nos_list,
)
self.assertEqual(manufacture_ste_doc.items[0].qty, 4.0)
def test_backflushed_serial_no_batch_raw_materials_based_on_transferred_autosabb(self):
frappe.db.set_single_value(
"Manufacturing Settings",
"backflush_raw_materials_based_on",
"Material Transferred for Manufacture",
)
sn_batch_item = "Test Batch Serial No WebCam"
fg_item = "Test FG Item with Serial & Batch No Raw Materials"
ste_doc = test_stock_entry.make_stock_entry(
item_code=sn_batch_item, target="Stores - _TC", qty=4, basic_rate=100, do_not_save=True
)
ste_doc.submit()
ste_doc.reload()
serial_nos_list = sorted(get_serial_nos_from_bundle(ste_doc.items[0].serial_and_batch_bundle))
batch_no = get_batch_from_bundle(ste_doc.items[0].serial_and_batch_bundle)
wo_doc = make_wo_order_test_record(production_item=fg_item, qty=4)
transferred_ste_doc = frappe.get_doc(
make_stock_entry(wo_doc.name, "Material Transfer for Manufacture", 4)
)
transferred_ste_doc.submit()
transferred_ste_doc.reload()
self.assertTrue(transferred_ste_doc.items[0].serial_and_batch_bundle)
self.assertEqual(
sorted(get_serial_nos_from_bundle(transferred_ste_doc.items[0].serial_and_batch_bundle)),
serial_nos_list,
)
self.assertEqual(
get_batch_from_bundle(transferred_ste_doc.items[0].serial_and_batch_bundle), batch_no
)
self.assertEqual(transferred_ste_doc.items[0].qty, 4.0)
manufacture_ste_doc = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 4))
manufacture_ste_doc.submit()
manufacture_ste_doc.reload()
self.assertTrue(manufacture_ste_doc.items[0].serial_and_batch_bundle)
self.assertEqual(
sorted(get_serial_nos_from_bundle(manufacture_ste_doc.items[0].serial_and_batch_bundle)),
serial_nos_list,
)
self.assertEqual(
get_batch_from_bundle(manufacture_ste_doc.items[0].serial_and_batch_bundle), batch_no
)
self.assertEqual(manufacture_ste_doc.items[0].qty, 4.0)
bundle = manufacture_ste_doc.items[0].serial_and_batch_bundle
bundle_doc = frappe.get_doc("Serial and Batch Bundle", bundle)
qty = sum(e.qty for e in bundle_doc.entries)
self.assertEqual(qty, -4.0)
###
def test_non_consumed_material_return_against_work_order(self):
frappe.db.set_single_value(
"Manufacturing Settings",
@@ -2335,6 +2526,29 @@ def prepare_data_for_backflush_based_on_materials_transferred():
make_bom(item=item.name, source_warehouse="Stores - _TC", raw_materials=[batch_item_doc.name])
# Make additional items not attached to a BOM
make_item(
"Test Batch Battery Consumable",
{
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "TBMK.#####",
"valuation_rate": 2.33,
"stock_uom": "Nos",
},
)
make_item(
"Test Serial Battery Consumable",
{
"is_stock_item": 1,
"has_serial_no": 1,
"serial_no_series": "TSBH.#####",
"valuation_rate": 3.33,
"stock_uom": "Nos",
},
)
sn_item_doc = make_item(
"Test Serial No BTT Headphone",
{

View File

@@ -377,3 +377,4 @@ erpnext.patches.v15_0.drop_index_posting_datetime_from_sle
erpnext.patches.v15_0.add_disassembly_order_stock_entry_type #1
erpnext.patches.v15_0.set_standard_stock_entry_type
erpnext.patches.v15_0.link_purchase_item_to_asset_doc
erpnext.patches.v14_0.update_currency_exchange_settings_for_frankfurter

View File

@@ -0,0 +1,11 @@
import frappe
def execute():
settings = frappe.get_doc("Currency Exchange Settings")
if settings.service_provider != "frankfurter.app":
return
settings.set_parameters_and_result()
settings.flags.ignore_validate = True
settings.save()

View File

@@ -342,12 +342,15 @@ erpnext.buying = {
add_serial_batch_bundle(doc, cdt, cdn) {
let item = locals[cdt][cdn];
let me = this;
let fields = ["has_batch_no", "has_serial_no"];
frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
frappe.db.get_value("Item", item.item_code, fields)
.then((r) => {
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
item.has_serial_no = r.message.has_serial_no;
item.has_batch_no = r.message.has_batch_no;
fields.forEach((field) => {
item[field] = r.message[field];
});
item.type_of_transaction = item.qty > 0 ? "Inward" : "Outward";
item.is_rejected = false;
@@ -380,12 +383,15 @@ erpnext.buying = {
add_serial_batch_for_rejected_qty(doc, cdt, cdn) {
let item = locals[cdt][cdn];
let me = this;
let fields = ["has_batch_no", "has_serial_no"];
frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
frappe.db.get_value("Item", item.item_code, fields)
.then((r) => {
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
item.has_serial_no = r.message.has_serial_no;
item.has_batch_no = r.message.has_batch_no;
fields.forEach((field) => {
item[field] = r.message[field];
});
item.type_of_transaction = item.rejected_qty > 0 ? "Inward" : "Outward";
item.is_rejected = true;

View File

@@ -128,7 +128,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
calculate_item_values() {
var me = this;
if (!this.discount_amount_applied) {
for (const item of this.frm._items || []) {
for (const item of this.frm.doc.items || []) {
frappe.model.round_floats_in(item);
item.net_rate = item.rate;
item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty;
@@ -227,7 +227,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
});
if(has_inclusive_tax==false) return;
$.each(me.frm._items || [], function(n, item) {
$.each(this.frm.doc.items || [], function(n, item) {
var item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
var cumulated_tax_fraction = 0.0;
var total_inclusive_tax_amount_per_qty = 0;
@@ -630,7 +630,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
_cleanup() {
this.frm.doc.base_in_words = this.frm.doc.in_words = "";
let items = this.frm._items;
let items = this.frm.doc.items;
if(items && items.length) {
if(!frappe.meta.get_docfield(items[0].doctype, "item_tax_amount", this.frm.doctype)) {

View File

@@ -573,6 +573,15 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
callback: function(r) {
if(!r.exc) {
frappe.run_serially([
() => {
if (item.docstatus === 0
&& frappe.meta.has_field(item.doctype, "use_serial_batch_fields")
&& !item.use_serial_batch_fields
&& cint(frappe.user_defaults?.use_serial_batch_fields) === 1
) {
item["use_serial_batch_fields"] = 1;
}
},
() => {
var d = locals[cdt][cdn];
me.add_taxes_from_item_tax_template(d.item_tax_rate);
@@ -1102,7 +1111,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
apply_discount_on_item(doc, cdt, cdn, field) {
var item = frappe.get_doc(cdt, cdn);
if(!item.price_list_rate) {
if(!item?.price_list_rate) {
item[field] = 0.0;
} else {
this.price_list_rate(doc, cdt, cdn);
@@ -1268,6 +1277,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
.filter(Boolean).length > 0;
} else if (this.frm.doc?.items) {
let first_row = this.frm.doc.items[0];
if (!first_row) {
return false
};
let mapped_rows = mappped_fields.filter(d => first_row[d])
return mapped_rows?.length > 0;

View File

@@ -16,7 +16,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
let label = this.item?.has_serial_no ? __("Serial Nos") : __("Batch Nos");
let primary_label = this.bundle ? __("Update") : __("Add");
if (this.item?.has_serial_no && this.item?.batch_no) {
if (this.item?.has_serial_no && this.item?.has_batch_no) {
label = __("Serial Nos / Batch Nos");
}
@@ -24,6 +24,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
this.dialog = new frappe.ui.Dialog({
title: this.item?.title || primary_label,
size: "large",
fields: this.get_dialog_fields(),
primary_action_label: primary_label,
primary_action: () => this.update_bundle_entries(),
@@ -164,12 +165,14 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
fields.push({
fieldtype: "Section Break",
depends_on: "eval:doc.enter_manually !== 1 || doc.entries?.length > 0",
});
fields.push({
fieldname: "entries",
fieldtype: "Table",
allow_bulk_edit: true,
depends_on: "eval:doc.enter_manually !== 1 || doc.entries?.length > 0",
data: [],
fields: this.get_dialog_table_fields(),
});
@@ -178,6 +181,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
}
get_attach_field() {
let me = this;
let label = this.item?.has_serial_no ? __("Serial Nos") : __("Batch Nos");
let primary_label = this.bundle ? __("Update") : __("Add");
@@ -185,66 +189,41 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
label = __("Serial Nos / Batch Nos");
}
let fields = [
{
fieldtype: "Section Break",
label: __("{0} {1} via CSV File", [primary_label, label]),
},
];
if (this.item?.has_serial_no) {
fields = [
...fields,
{
fieldtype: "Check",
label: __("Import Using CSV file"),
fieldname: "import_using_csv_file",
default: 0,
let fields = [];
if (this.item.has_serial_no) {
fields.push({
fieldtype: "Check",
label: __("Enter Manually"),
fieldname: "enter_manually",
default: 1,
depends_on: "eval:doc.import_using_csv_file !== 1",
change() {
if (me.dialog.get_value("enter_manually")) {
me.dialog.set_value("import_using_csv_file", 0);
}
},
{
fieldtype: "Section Break",
label: __("{0} {1} Manually", [primary_label, label]),
depends_on: "eval:doc.import_using_csv_file === 0",
},
{
fieldtype: "Data",
label: __("Enter Serial No Range"),
fieldname: "serial_no_range",
depends_on: "eval:doc.import_using_csv_file === 0",
description: __('Enter "ABC-001::100" for serial nos "ABC-001" to "ABC-100".'),
onchange: () => {
this.set_serial_nos_from_range();
},
},
{
fieldtype: "Small Text",
label: __("Enter Serial Nos"),
fieldname: "upload_serial_nos",
depends_on: "eval:doc.import_using_csv_file === 0",
description: __("Enter each serial no in a new line"),
},
{
fieldtype: "Column Break",
depends_on: "eval:doc.import_using_csv_file === 0",
},
{
fieldtype: "Button",
fieldname: "make_serial_nos",
label: __("Create Serial Nos"),
depends_on: "eval:doc.import_using_csv_file === 0",
click: () => {
this.create_serial_nos();
},
},
{
fieldtype: "Section Break",
depends_on: "eval:doc.import_using_csv_file === 1",
},
];
});
}
fields = [
...fields,
{
fieldtype: "Check",
label: __("Import Using CSV file"),
fieldname: "import_using_csv_file",
depends_on: "eval:doc.enter_manually !== 1",
default: !this.item.has_serial_no ? 1 : 0,
change() {
if (me.dialog.get_value("import_using_csv_file")) {
me.dialog.set_value("enter_manually", 0);
}
},
},
{
fieldtype: "Section Break",
depends_on: "eval:doc.import_using_csv_file === 1",
label: __("{0} {1} via CSV File", [primary_label, label]),
},
{
fieldtype: "Button",
fieldname: "download_csv",
@@ -262,9 +241,51 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
},
];
if (this.item?.has_serial_no) {
fields = [
...fields,
{
fieldtype: "Section Break",
label: __("{0} {1} Manually", [primary_label, label]),
depends_on: "eval:doc.enter_manually === 1",
},
{
fieldtype: "Data",
label: __("Serial No Range"),
fieldname: "serial_no_range",
depends_on: "eval:doc.enter_manually === 1 && !doc.serial_no_series",
description: __('"SN-01::10" for "SN-01" to "SN-10"'),
onchange: () => {
this.set_serial_nos_from_range();
},
},
];
}
if (this.item?.has_serial_no) {
fields = [
...fields,
{
fieldtype: "Column Break",
depends_on: "eval:doc.enter_manually === 1",
},
{
fieldtype: "Small Text",
label: __("Enter Serial Nos"),
fieldname: "upload_serial_nos",
depends_on: "eval:doc.enter_manually === 1",
description: __("Enter each serial no in a new line"),
},
];
}
return fields;
}
set_serial_nos_from_series() {}
set_batch_nos_from_series() {}
set_serial_nos_from_range() {
const serial_no_range = this.dialog.get_value("serial_no_range");
@@ -511,6 +532,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
scan_barcode_data() {
const { scan_serial_no, scan_batch_no } = this.dialog.get_values();
this.dialog.set_value("enter_manually", 0);
if (scan_serial_no || scan_batch_no) {
frappe.call({
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.is_serial_batch_no_exists",
@@ -554,14 +577,13 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
serial_no: scan_serial_no,
},
callback: (r) => {
if (r.message) {
this.dialog.fields_dict.entries.df.data.push({
serial_no: scan_serial_no,
batch_no: r.message,
});
this.dialog.fields_dict.entries.df.data.push({
serial_no: scan_serial_no,
batch_no: r.message,
});
this.dialog.fields_dict.scan_serial_no.set_value("");
}
this.dialog.fields_dict.scan_serial_no.set_value("");
this.dialog.fields_dict.entries.grid.refresh();
},
});
}
@@ -590,6 +612,12 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
update_bundle_entries() {
let entries = this.dialog.get_values().entries;
let warehouse = this.dialog.get_value("warehouse");
let upload_serial_nos = this.dialog.get_value("upload_serial_nos");
if (!entries?.length && upload_serial_nos) {
this.create_serial_nos();
return;
}
if ((entries && !entries.length) || !entries) {
frappe.throw(__("Please add atleast one Serial No / Batch No"));
@@ -610,9 +638,13 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
},
})
.then((r) => {
this.callback && this.callback(r.message);
this.frm.save();
this.dialog.hide();
frappe.run_serially([
() => {
this.callback && this.callback(r.message);
},
() => this.frm.save(),
() => this.dialog.hide(),
]);
});
}

View File

@@ -561,12 +561,50 @@ class TestQuotation(FrappeTestCase):
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"rate": 10,
"included_in_print_rate": 1,
},
)
quotation.submit()
self.assertEqual(quotation.net_total, 290)
self.assertEqual(quotation.grand_total, 319)
self.assertEqual(round(quotation.items[1].net_rate, 2), 136.36)
self.assertEqual(round(quotation.items[1].amount, 2), 150)
self.assertEqual(round(quotation.items[2].net_rate, 2), 163.64)
self.assertEqual(round(quotation.items[2].amount, 2), 180)
self.assertEqual(round(quotation.net_total, 2), 263.64)
self.assertEqual(round(quotation.total_taxes_and_charges, 2), 26.36)
self.assertEqual(quotation.grand_total, 290)
def test_amount_calculation_for_alternative_items(self):
"""Make sure that the amount is calculated correctly for alternative items when the qty is changed."""
from erpnext.stock.doctype.item.test_item import make_item
item_list = []
stock_items = {
"_Test Simple Item 1": 100,
"_Test Alt 1": 120,
}
for item, rate in stock_items.items():
make_item(item, {"is_stock_item": 0})
item_list.append(
{
"item_code": item,
"qty": 1,
"rate": rate,
"is_alternative": "Alt" in item,
}
)
quotation = make_quotation(item_list=item_list, do_not_submit=1)
self.assertEqual(quotation.items[1].amount, 120)
quotation.items[1].qty = 2
quotation.save()
self.assertEqual(quotation.items[1].amount, 240)
def test_alternative_items_sales_order_mapping_with_stock_items(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order

View File

@@ -68,9 +68,9 @@ def patched_requests_get(*args, **kwargs):
if kwargs["params"].get("date") and kwargs["params"].get("from") and kwargs["params"].get("to"):
if test_exchange_values.get(kwargs["params"]["date"]):
return PatchResponse({"result": test_exchange_values[kwargs["params"]["date"]]}, 200)
elif args[0].startswith("https://frankfurter.app") and kwargs.get("params"):
elif args[0].startswith("https://api.frankfurter.app") and kwargs.get("params"):
if kwargs["params"].get("base") and kwargs["params"].get("symbols"):
date = args[0].replace("https://frankfurter.app/", "")
date = args[0].replace("https://api.frankfurter.app/", "")
if test_exchange_values.get(date):
return PatchResponse(
{"rates": {kwargs["params"].get("symbols"): test_exchange_values.get(date)}}, 200

View File

@@ -98,7 +98,7 @@ def setup_currency_exchange():
ces.set("result_key", [])
ces.set("req_params", [])
ces.api_endpoint = "https://frankfurter.app/{transaction_date}"
ces.api_endpoint = "https://api.frankfurter.app/{transaction_date}"
ces.append("result_key", {"key": "rates"})
ces.append("result_key", {"key": "{to_currency}"})
ces.append("req_params", {"key": "base", "value": "{from_currency}"})

View File

@@ -48,17 +48,18 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
let actual_qty = unescape(element.attr("data-actual_qty"));
let disable_quick_entry = Number(unescape(element.attr("data-disable_quick_entry")));
let entry_type = action === "Move" ? "Material Transfer" : "Material Receipt";
let stock_uom = unescape(element.attr("data-stock-uom"));
if (disable_quick_entry) {
open_stock_entry(item, warehouse, entry_type);
} else {
if (action === "Add") {
let rate = unescape($(this).attr("data-rate"));
erpnext.stock.move_item(item, null, warehouse, actual_qty, rate, function () {
erpnext.stock.move_item(item, null, warehouse, actual_qty, rate, stock_uom, function () {
me.refresh();
});
} else {
erpnext.stock.move_item(item, warehouse, null, actual_qty, null, function () {
erpnext.stock.move_item(item, warehouse, null, actual_qty, null, stock_uom, function () {
me.refresh();
});
}
@@ -207,7 +208,7 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
}
};
erpnext.stock.move_item = function (item, source, target, actual_qty, rate, callback) {
erpnext.stock.move_item = function (item, source, target, actual_qty, rate, stock_uom, callback) {
var dialog = new frappe.ui.Dialog({
title: target ? __("Add Item") : __("Move Item"),
fields: [
@@ -295,6 +296,8 @@ erpnext.stock.move_item = function (item, source, target, actual_qty, rate, call
let row = frappe.model.add_child(doc, "items");
row.item_code = dialog.get_value("item_code");
row.s_warehouse = dialog.get_value("source");
row.stock_uom = stock_uom;
row.uom = stock_uom;
row.t_warehouse = dialog.get_value("target");
row.qty = dialog.get_value("qty");
row.conversion_factor = 1;

View File

@@ -71,6 +71,7 @@ def get_data(
item.update(
{
"item_name": frappe.get_cached_value("Item", item.item_code, "item_name"),
"stock_uom": frappe.get_cached_value("Item", item.item_code, "stock_uom"),
"disable_quick_entry": frappe.get_cached_value("Item", item.item_code, "has_batch_no")
or frappe.get_cached_value("Item", item.item_code, "has_serial_no"),
"projected_qty": flt(item.projected_qty, precision),

View File

@@ -49,12 +49,14 @@
data-disable_quick_entry="{{ d.disable_quick_entry }}"
data-warehouse="{{ d.warehouse }}"
data-actual_qty="{{ d.actual_qty }}"
data-stock-uom="{{ d.stock_uom }}"
data-item="{{ escape(d.item_code) }}">{{ __("Move") }}</a>
{% endif %}
<button style="margin-left: 7px;" class="btn btn-default btn-xs btn-add"
data-disable_quick_entry="{{ d.disable_quick_entry }}"
data-warehouse="{{ d.warehouse }}"
data-actual_qty="{{ d.actual_qty }}"
data-stock-uom="{{ d.stock_uom }}"
data-item="{{ escape(d.item_code) }}"
data-rate="{{ d.valuation_rate }}">{{ __("Add") }}</a>
</div>

View File

@@ -22,6 +22,7 @@ from frappe.utils import (
strip_html,
)
from frappe.utils.html_utils import clean_html
from pypika import Order
import erpnext
from erpnext.controllers.item_variant import (
@@ -345,7 +346,13 @@ class Item(Document):
def validate_item_tax_net_rate_range(self):
for tax in self.get("taxes"):
if flt(tax.maximum_net_rate) < flt(tax.minimum_net_rate):
frappe.throw(_("Row #{0}: Maximum Net Rate cannot be greater than Minimum Net Rate"))
frappe.throw(
_("Taxes row #{0}: {1} cannot be smaller than {2}").format(
tax.idx,
bold(_(tax.meta.get_label("maximum_net_rate"))),
bold(_(tax.meta.get_label("minimum_net_rate"))),
)
)
def update_template_tables(self):
template = frappe.get_cached_doc("Item", self.variant_of)
@@ -1133,34 +1140,10 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
"""returns last purchase details in stock uom"""
# get last purchase order item details
last_purchase_order = frappe.db.sql(
"""\
select po.name, po.transaction_date, po.conversion_rate,
po_item.conversion_factor, po_item.base_price_list_rate,
po_item.discount_percentage, po_item.base_rate, po_item.base_net_rate
from `tabPurchase Order` po, `tabPurchase Order Item` po_item
where po.docstatus = 1 and po_item.item_code = %s and po.name != %s and
po.name = po_item.parent
order by po.transaction_date desc, po.name desc
limit 1""",
(item_code, cstr(doc_name)),
as_dict=1,
)
last_purchase_order = get_purchase_voucher_details("Purchase Order", item_code, doc_name)
# get last purchase receipt item details
last_purchase_receipt = frappe.db.sql(
"""\
select pr.name, pr.posting_date, pr.posting_time, pr.conversion_rate,
pr_item.conversion_factor, pr_item.base_price_list_rate, pr_item.discount_percentage,
pr_item.base_rate, pr_item.base_net_rate
from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pr_item
where pr.docstatus = 1 and pr_item.item_code = %s and pr.name != %s and
pr.name = pr_item.parent
order by pr.posting_date desc, pr.posting_time desc, pr.name desc
limit 1""",
(item_code, cstr(doc_name)),
as_dict=1,
)
last_purchase_receipt = get_purchase_voucher_details("Purchase Receipt", item_code, doc_name)
purchase_order_date = getdate(
last_purchase_order and last_purchase_order[0].transaction_date or "1900-01-01"
@@ -1181,7 +1164,13 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
purchase_date = purchase_receipt_date
else:
return frappe._dict()
last_purchase_invoice = get_purchase_voucher_details("Purchase Invoice", item_code, doc_name)
if last_purchase_invoice:
last_purchase = last_purchase_invoice[0]
purchase_date = getdate(last_purchase.posting_date)
else:
return frappe._dict()
conversion_factor = flt(last_purchase.conversion_factor)
out = frappe._dict(
@@ -1207,6 +1196,40 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0):
return out
def get_purchase_voucher_details(doctype, item_code, document_name):
parent_doc = frappe.qb.DocType(doctype)
child_doc = frappe.qb.DocType(doctype + " Item")
query = (
frappe.qb.from_(parent_doc)
.inner_join(child_doc)
.on(parent_doc.name == child_doc.parent)
.select(
parent_doc.name,
parent_doc.conversion_rate,
child_doc.conversion_factor,
child_doc.base_price_list_rate,
child_doc.discount_percentage,
child_doc.base_rate,
child_doc.base_net_rate,
)
.where(parent_doc.docstatus == 1)
.where(child_doc.item_code == item_code)
.where(parent_doc.name != document_name)
)
if doctype in ("Purchase Receipt", "Purchase Invoice"):
query = query.select(parent_doc.posting_date, parent_doc.posting_time)
query = query.orderby(
parent_doc.posting_date, parent_doc.posting_time, parent_doc.name, order=Order.desc
)
else:
query = query.select(parent_doc.transaction_date)
query = query.orderby(parent_doc.transaction_date, parent_doc.name, order=Order.desc)
return query.run(as_dict=1)
def check_stock_uom_with_bin(item, stock_uom):
if stock_uom == frappe.db.get_value("Item", item, "stock_uom"):
return

View File

@@ -245,6 +245,7 @@ class QualityInspection(Document):
for i in range(1, 11):
field = "reading_" + str(i)
if reading.get(field) is None:
data[field] = 0.0
continue
data[field] = parse_float(reading.get(field))

View File

@@ -280,7 +280,7 @@ class SerialandBatchBundle(Document):
)
def validate_negative_batch(self, batch_no, available_qty):
if available_qty < 0:
if available_qty < 0 and not self.is_stock_reco_for_valuation_adjustment(available_qty):
msg = f"""Batch No {bold(batch_no)} of an Item {bold(self.item_code)}
has negative stock
of quantity {bold(available_qty)} in the
@@ -288,6 +288,18 @@ class SerialandBatchBundle(Document):
frappe.throw(_(msg), BatchNegativeStockError)
def is_stock_reco_for_valuation_adjustment(self, available_qty):
if (
self.voucher_type == "Stock Reconciliation"
and self.type_of_transaction == "Outward"
and self.voucher_detail_no
and abs(frappe.db.get_value("Stock Reconciliation Item", self.voucher_detail_no, "qty"))
== abs(available_qty)
):
return True
return False
def get_sle_for_outward_transaction(self):
sle = frappe._dict(
{
@@ -1329,6 +1341,15 @@ def create_serial_batch_no_ledgers(
}
)
batch_no = None
if (
not entries[0].get("batch_no")
and entries[0].get("serial_no")
and frappe.get_cached_value("Item", child_row.item_code, "has_batch_no")
):
batch_no = get_batch(child_row.item_code)
for row in entries:
row = frappe._dict(row)
doc.append(
@@ -1336,7 +1357,7 @@ def create_serial_batch_no_ledgers(
{
"qty": (flt(row.qty) or 1.0) * (1 if type_of_transaction == "Inward" else -1),
"warehouse": warehouse,
"batch_no": row.batch_no,
"batch_no": row.batch_no or batch_no,
"serial_no": row.serial_no,
},
)
@@ -1351,6 +1372,18 @@ def create_serial_batch_no_ledgers(
return doc
def get_batch(item_code):
from erpnext.stock.doctype.batch.batch import make_batch
return make_batch(
frappe._dict(
{
"item": item_code,
}
)
)
def get_type_of_transaction(parent_doc, child_row):
type_of_transaction = child_row.get("type_of_transaction")
if parent_doc.get("doctype") == "Stock Entry":

View File

@@ -1594,10 +1594,6 @@ class StockEntry(StockController):
if pro_doc.status == "Stopped":
msg = f"Transaction not allowed against stopped Work Order {self.work_order}"
if self.is_return and pro_doc.status not in ["Completed", "Closed"]:
title = _("Stock Return")
msg = f"Work Order {self.work_order} must be completed or closed"
if msg:
frappe.throw(_(msg), title=title)

View File

@@ -61,6 +61,7 @@ class StockReconciliation(StockController):
self.head_row = ["Item Code", "Warehouse", "Quantity", "Valuation Rate"]
def validate(self):
self.validate_items_exist()
if not self.expense_account:
self.expense_account = frappe.get_cached_value(
"Company", self.company, "stock_adjustment_account"
@@ -162,6 +163,9 @@ class StockReconciliation(StockController):
def set_current_serial_and_batch_bundle(self, voucher_detail_no=None, save=False) -> None:
"""Set Serial and Batch Bundle for each item"""
for item in self.items:
if not frappe.db.exists("Item", item.item_code):
frappe.throw(_("Item {0} does not exist").format(item.item_code))
if not item.reconcile_all_serial_batch and item.serial_and_batch_bundle:
bundle = self.get_bundle_for_specific_serial_batch(item)
item.current_serial_and_batch_bundle = bundle.name
@@ -357,6 +361,9 @@ class StockReconciliation(StockController):
def set_new_serial_and_batch_bundle(self):
for item in self.items:
if not item.item_code:
continue
if item.use_serial_batch_fields:
continue
@@ -684,7 +691,7 @@ class StockReconciliation(StockController):
from erpnext.stock.stock_ledger import get_stock_value_difference
difference_amount = get_stock_value_difference(
row.item_code, row.warehouse, self.posting_date, self.posting_time
row.item_code, row.warehouse, self.posting_date, self.posting_time, self.name
)
if not difference_amount:

View File

@@ -1,30 +1,33 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2013-01-14 10:52:58",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 3,
"is_standard": "Yes",
"json": "{\"add_total_row\": 0, \"sort_by\": \"Serial No.modified\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [[\"Serial No\", \"delivery_document_type\", \"in\", [\"Delivery Note\", \"Sales Invoice\"]], [\"Serial No\", \"warehouse\", \"=\", \"\"]], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"amc_expiry_date\", \"Serial No\"], [\"maintenance_status\", \"Serial No\"], [\"delivery_document_no\", \"Serial No\"], [\"customer\", \"Serial No\"], [\"customer_name\", \"Serial No\"], [\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"]]}",
"modified": "2017-02-24 20:02:00.706889",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial No Service Contract Expiry",
"owner": "Administrator",
"ref_doctype": "Serial No",
"report_name": "Serial No Service Contract Expiry",
"report_type": "Report Builder",
"add_total_row": 0,
"columns": [],
"creation": "2013-01-14 10:52:58",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 3,
"is_standard": "Yes",
"json": "{\"add_total_row\": 0, \"sort_by\": \"Serial No.modified\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [[\"Serial No\", \"warehouse\", \"=\", \"\"]], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"amc_expiry_date\", \"Serial No\"], [\"maintenance_status\", \"Serial No\"],[\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"]]}",
"letterhead": null,
"modified": "2024-09-26 13:07:23.451182",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial No Service Contract Expiry",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Serial No",
"report_name": "Serial No Service Contract Expiry",
"report_type": "Report Builder",
"roles": [
{
"role": "Item Manager"
},
},
{
"role": "Stock Manager"
},
},
{
"role": "Stock User"
}
]
}
}

View File

@@ -1,30 +1,33 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2013-01-14 10:52:58",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 4,
"is_standard": "Yes",
"json": "{\"add_total_row\": 0, \"sort_by\": \"Serial No.name\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"warehouse\", \"Serial No\"], [\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"], [\"purchase_document_type\", \"Serial No\"], [\"purchase_document_no\", \"Serial No\"], [\"purchase_date\", \"Serial No\"], [\"customer\", \"Serial No\"], [\"customer_name\", \"Serial No\"], [\"purchase_rate\", \"Serial No\"], [\"delivery_document_type\", \"Serial No\"], [\"delivery_document_no\", \"Serial No\"], [\"delivery_date\", \"Serial No\"], [\"supplier\", \"Serial No\"], [\"supplier_name\", \"Serial No\"]]}",
"modified": "2017-02-24 19:54:21.392265",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial No Status",
"owner": "Administrator",
"ref_doctype": "Serial No",
"report_name": "Serial No Status",
"report_type": "Report Builder",
"add_total_row": 0,
"columns": [],
"creation": "2013-01-14 10:52:58",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 4,
"is_standard": "Yes",
"json": "{\"add_total_row\": 0, \"sort_by\": \"Serial No.name\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"warehouse\", \"Serial No\"], [\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"],[\"purchase_document_no\", \"Serial No\"]]}",
"letterhead": null,
"modified": "2024-09-26 13:10:52.693648",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial No Status",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Serial No",
"report_name": "Serial No Status",
"report_type": "Report Builder",
"roles": [
{
"role": "Item Manager"
},
},
{
"role": "Stock Manager"
},
},
{
"role": "Stock User"
}
]
}
}

View File

@@ -1,30 +1,33 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2013-01-14 10:52:58",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 3,
"is_standard": "Yes",
"json": "{\"add_total_row\": 0, \"sort_by\": \"Serial No.modified\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [[\"Serial No\", \"delivery_document_type\", \"in\", [\"Delivery Note\", \"Sales Invoice\"]], [\"Serial No\", \"warehouse\", \"=\", \"\"]], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"warranty_expiry_date\", \"Serial No\"], [\"warranty_period\", \"Serial No\"], [\"maintenance_status\", \"Serial No\"], [\"purchase_document_no\", \"Serial No\"], [\"purchase_date\", \"Serial No\"], [\"supplier\", \"Serial No\"], [\"supplier_name\", \"Serial No\"], [\"delivery_document_no\", \"Serial No\"], [\"delivery_date\", \"Serial No\"], [\"customer\", \"Serial No\"], [\"customer_name\", \"Serial No\"], [\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"]]}",
"modified": "2017-02-24 20:01:53.097456",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial No Warranty Expiry",
"owner": "Administrator",
"ref_doctype": "Serial No",
"report_name": "Serial No Warranty Expiry",
"report_type": "Report Builder",
"add_total_row": 0,
"columns": [],
"creation": "2013-01-14 10:52:58",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 3,
"is_standard": "Yes",
"json": "{\"add_total_row\": 0, \"sort_by\": \"Serial No.modified\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [[\"Serial No\", \"warehouse\", \"=\", \"\"]], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"amc_expiry_date\", \"Serial No\"], [\"maintenance_status\", \"Serial No\"],[\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"]]}",
"letterhead": null,
"modified": "2024-09-26 13:07:23.451182",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial No Service Contract Expiry",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Serial No",
"report_name": "Serial No Service Contract Expiry",
"report_type": "Report Builder",
"roles": [
{
"role": "Item Manager"
},
},
{
"role": "Stock Manager"
},
},
{
"role": "Stock User"
}
]
}
}

View File

@@ -52,7 +52,7 @@ def add_invariant_check_fields(sles):
balance_qty = 0.0
balance_stock_value = 0.0
for idx, sle in enumerate(sles):
queue = json.loads(sle.stock_queue)
queue = json.loads(sle.stock_queue) if sle.stock_queue else []
fifo_qty = 0.0
fifo_value = 0.0

View File

@@ -3,6 +3,15 @@
frappe.query_reports["Warehouse wise Item Balance Age and Value"] = {
filters: [
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
width: "80",
options: "Company",
reqd: 1,
default: frappe.defaults.get_user_default("Company"),
},
{
fieldname: "from_date",
label: __("From Date"),
@@ -39,6 +48,12 @@ frappe.query_reports["Warehouse wise Item Balance Age and Value"] = {
fieldtype: "Link",
width: "80",
options: "Warehouse",
get_query: function () {
const company = frappe.query_report.get_filter_value("company");
return {
filters: { company: company },
};
},
},
{
fieldname: "filter_total_zero_qty",

View File

@@ -109,8 +109,6 @@ def validate_filters(filters):
sle_count = flt(frappe.qb.from_("Stock Ledger Entry").select(Count("name")).run()[0][0])
if sle_count > 500000:
frappe.throw(_("Please set filter based on Item or Warehouse"))
if not filters.get("company"):
filters["company"] = frappe.defaults.get_user_default("Company")
def get_warehouse_list(filters):

View File

@@ -865,7 +865,7 @@ class update_entries_after:
sle.stock_value = self.wh_data.stock_value
sle.stock_queue = json.dumps(self.wh_data.stock_queue)
if not sle.is_adjustment_entry or not self.args.get("sle_id"):
if not sle.is_adjustment_entry:
sle.stock_value_difference = stock_value_difference
sle.doctype = "Stock Ledger Entry"

View File

@@ -137,6 +137,11 @@ def task(doc_name, from_doctype, to_doctype):
},
"Purchase Receipt": {"Purchase Invoice": purchase_receipt.make_purchase_invoice},
}
hooks = frappe.get_hooks("bulk_transaction_task_mapper")
for hook in hooks:
mapper.update(frappe.get_attr(hook)())
frappe.flags.bulk_transaction = True
if to_doctype in ["Payment Entry"]:
obj = mapper[from_doctype][to_doctype](from_doctype, doc_name)