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

chore: release v15
This commit is contained in:
Deepesh Garg
2024-04-03 16:01:44 +05:30
committed by GitHub
39 changed files with 498 additions and 234 deletions

View File

@@ -453,7 +453,10 @@ frappe.ui.form.on("Journal Entry Account", {
} }
}, },
cost_center: function (frm, dt, dn) { cost_center: function (frm, dt, dn) {
erpnext.journal_entry.set_account_details(frm, dt, dn); // Don't reset for Gain/Loss type journals, as it will make Debit and Credit values '0'
if (frm.doc.voucher_type != "Exchange Gain Or Loss") {
erpnext.journal_entry.set_account_details(frm, dt, dn);
}
}, },
account: function (frm, dt, dn) { account: function (frm, dt, dn) {

View File

@@ -579,6 +579,7 @@
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 1, "hidden": 1,
"label": "Payment Order Status", "label": "Payment Order Status",
"no_copy": 1,
"options": "Initiated\nPayment Ordered", "options": "Initiated\nPayment Ordered",
"read_only": 1 "read_only": 1
}, },
@@ -821,4 +822,4 @@
"states": [], "states": [],
"title_field": "title", "title_field": "title",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -671,7 +671,7 @@ class ReceivablePayableReport(object):
else: else:
future_amount_field = "future_amount_in_base_currency" future_amount_field = "future_amount_in_base_currency"
if row.remaining_balance > 0 and future.get(future_amount_field): if row.remaining_balance != 0 and future.get(future_amount_field):
if future.get(future_amount_field) > row.outstanding: if future.get(future_amount_field) > row.outstanding:
row.future_amount = row.outstanding row.future_amount = row.outstanding
future[future_amount_field] = future.get(future_amount_field) - row.outstanding future[future_amount_field] = future.get(future_amount_field) - row.outstanding

View File

@@ -469,11 +469,30 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
) )
def test_future_payments(self): def test_future_payments(self):
sr = self.create_sales_invoice(do_not_submit=True)
sr.is_return = 1
sr.items[0].qty = -1
sr.items[0].rate = 10
sr.calculate_taxes_and_totals()
sr.submit()
si = self.create_sales_invoice() si = self.create_sales_invoice()
pe = get_payment_entry(si.doctype, si.name) pe = get_payment_entry(si.doctype, si.name)
pe.append(
"references",
{
"reference_doctype": sr.doctype,
"reference_name": sr.name,
"due_date": sr.due_date,
"total_amount": sr.grand_total,
"outstanding_amount": sr.outstanding_amount,
"allocated_amount": sr.outstanding_amount,
},
)
pe.posting_date = add_days(today(), 1) pe.posting_date = add_days(today(), 1)
pe.paid_amount = 90.0 pe.paid_amount = 80
pe.references[0].allocated_amount = 90.0 pe.references[0].allocated_amount = 90.0 # pe.paid_amount + sr.grand_total
pe.save().submit() pe.save().submit()
filters = { filters = {
"company": self.company, "company": self.company,
@@ -485,16 +504,21 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"show_future_payments": True, "show_future_payments": True,
} }
report = execute(filters)[1] report = execute(filters)[1]
self.assertEqual(len(report), 1) self.assertEqual(len(report), 2)
expected_data = [100.0, 100.0, 10.0, 90.0] expected_data = {sr.name: [10.0, -10.0, 0.0, -10], si.name: [100.0, 100.0, 10.0, 90.0]}
row = report[0] rows = report[:2]
self.assertEqual( for row in rows:
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount] self.assertEqual(
) expected_data[row.voucher_no],
[row.invoiced or row.paid, row.outstanding, row.remaining_balance, row.future_amount],
)
pe.cancel() pe.cancel()
sr.load_from_db() # Outstanding amount is updated so a updated timestamp is needed.
sr.cancel()
# full payment in future date # full payment in future date
pe = get_payment_entry(si.doctype, si.name) pe = get_payment_entry(si.doctype, si.name)
pe.posting_date = add_days(today(), 1) pe.posting_date = add_days(today(), 1)

View File

@@ -462,7 +462,7 @@
}, },
{ {
"fieldname": "other_charges_calculation", "fieldname": "other_charges_calculation",
"fieldtype": "Markdown Editor", "fieldtype": "Text Editor",
"label": "Taxes and Charges Calculation", "label": "Taxes and Charges Calculation",
"no_copy": 1, "no_copy": 1,
"oldfieldtype": "HTML", "oldfieldtype": "HTML",
@@ -928,7 +928,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2024-03-20 16:03:59.069145", "modified": "2024-03-28 10:20:30.231915",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Supplier Quotation", "name": "Supplier Quotation",

View File

@@ -71,7 +71,7 @@ class SupplierQuotation(BuyingController):
naming_series: DF.Literal["PUR-SQTN-.YYYY.-"] naming_series: DF.Literal["PUR-SQTN-.YYYY.-"]
net_total: DF.Currency net_total: DF.Currency
opportunity: DF.Link | None opportunity: DF.Link | None
other_charges_calculation: DF.MarkdownEditor | None other_charges_calculation: DF.TextEditor | None
plc_conversion_rate: DF.Float plc_conversion_rate: DF.Float
price_list_currency: DF.Link | None price_list_currency: DF.Link | None
pricing_rules: DF.Table[PricingRuleDetail] pricing_rules: DF.Table[PricingRuleDetail]

View File

@@ -750,12 +750,12 @@ def get_serial_and_batch_bundle(child, parent):
"item_code": child.item_code, "item_code": child.item_code,
"warehouse": child.warehouse, "warehouse": child.warehouse,
"voucher_type": parent.doctype, "voucher_type": parent.doctype,
"voucher_no": parent.name, "voucher_no": parent.name if parent.docstatus < 2 else None,
"voucher_detail_no": child.name, "voucher_detail_no": child.name,
"posting_date": parent.posting_date, "posting_date": parent.posting_date,
"posting_time": parent.posting_time, "posting_time": parent.posting_time,
"qty": child.qty, "qty": child.qty,
"type_of_transaction": "Outward" if child.qty > 0 else "Inward", "type_of_transaction": "Outward" if child.qty > 0 and parent.docstatus < 2 else "Inward",
"company": parent.company, "company": parent.company,
"do_not_submit": "True", "do_not_submit": "True",
} }

View File

@@ -913,7 +913,7 @@ class StockController(AccountsController):
self.validate_multi_currency() self.validate_multi_currency()
self.validate_packed_items() self.validate_packed_items()
if self.get("is_internal_supplier"): if self.get("is_internal_supplier") and self.docstatus == 1:
self.validate_internal_transfer_qty() self.validate_internal_transfer_qty()
else: else:
self.validate_internal_transfer_warehouse() self.validate_internal_transfer_warehouse()

View File

@@ -238,7 +238,7 @@
"fieldname": "rm_cost_as_per", "fieldname": "rm_cost_as_per",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Rate Of Materials Based On", "label": "Rate Of Materials Based On",
"options": "Valuation Rate\nLast Purchase Rate\nPrice List\nManual" "options": "Valuation Rate\nLast Purchase Rate\nPrice List"
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
@@ -637,7 +637,7 @@
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-12-26 19:34:08.159312", "modified": "2024-04-02 16:22:47.518411",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM", "name": "BOM",
@@ -676,4 +676,4 @@
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -150,7 +150,7 @@ class BOM(WebsiteGenerator):
quality_inspection_template: DF.Link | None quality_inspection_template: DF.Link | None
quantity: DF.Float quantity: DF.Float
raw_material_cost: DF.Currency raw_material_cost: DF.Currency
rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List", "Manual"] rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List"]
route: DF.SmallText | None route: DF.SmallText | None
routing: DF.Link | None routing: DF.Link | None
scrap_items: DF.Table[BOMScrapItem] scrap_items: DF.Table[BOMScrapItem]
@@ -742,6 +742,7 @@ class BOM(WebsiteGenerator):
def calculate_rm_cost(self, save=False): def calculate_rm_cost(self, save=False):
"""Fetch RM rate as per today's valuation rate and calculate totals""" """Fetch RM rate as per today's valuation rate and calculate totals"""
total_rm_cost = 0 total_rm_cost = 0
base_total_rm_cost = 0 base_total_rm_cost = 0
@@ -750,7 +751,7 @@ class BOM(WebsiteGenerator):
continue continue
old_rate = d.rate old_rate = d.rate
if self.rm_cost_as_per != "Manual": if not self.bom_creator:
d.rate = self.get_rm_rate( d.rate = self.get_rm_rate(
{ {
"company": self.company, "company": self.company,
@@ -1022,8 +1023,6 @@ def get_bom_item_rate(args, bom_doc):
item_doc = frappe.get_cached_doc("Item", args.get("item_code")) item_doc = frappe.get_cached_doc("Item", args.get("item_code"))
price_list_data = get_price_list_rate(bom_args, item_doc) price_list_data = get_price_list_rate(bom_args, item_doc)
rate = price_list_data.price_list_rate rate = price_list_data.price_list_rate
elif bom_doc.rm_cost_as_per == "Manual":
return
return flt(rate) return flt(rate)

View File

@@ -66,7 +66,7 @@
"fieldname": "rm_cost_as_per", "fieldname": "rm_cost_as_per",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Rate Of Materials Based On", "label": "Rate Of Materials Based On",
"options": "Valuation Rate\nLast Purchase Rate\nPrice List\nManual", "options": "Valuation Rate\nLast Purchase Rate\nPrice List",
"reqd": 1 "reqd": 1
}, },
{ {
@@ -288,7 +288,7 @@
"link_fieldname": "bom_creator" "link_fieldname": "bom_creator"
} }
], ],
"modified": "2023-08-07 15:45:06.176313", "modified": "2024-04-02 16:30:59.779190",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM Creator", "name": "BOM Creator",
@@ -327,4 +327,4 @@
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -59,7 +59,7 @@ class BOMCreator(Document):
qty: DF.Float qty: DF.Float
raw_material_cost: DF.Currency raw_material_cost: DF.Currency
remarks: DF.TextEditor | None remarks: DF.TextEditor | None
rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List", "Manual"] rm_cost_as_per: DF.Literal["Valuation Rate", "Last Purchase Rate", "Price List"]
set_rate_based_on_warehouse: DF.Check set_rate_based_on_warehouse: DF.Check
status: DF.Literal["Draft", "Submitted", "In Progress", "Completed", "Failed", "Cancelled"] status: DF.Literal["Draft", "Submitted", "In Progress", "Completed", "Failed", "Cancelled"]
uom: DF.Link | None uom: DF.Link | None
@@ -143,9 +143,6 @@ class BOMCreator(Document):
self.submit() self.submit()
def set_rate_for_items(self): def set_rate_for_items(self):
if self.rm_cost_as_per == "Manual":
return
amount = self.get_raw_material_cost() amount = self.get_raw_material_cost()
self.raw_material_cost = amount self.raw_material_cost = amount
@@ -240,6 +237,9 @@ class BOMCreator(Document):
(row.item_code, row.name), frappe._dict({"items": [], "bom_no": "", "fg_item_data": row}) (row.item_code, row.name), frappe._dict({"items": [], "bom_no": "", "fg_item_data": row})
) )
if not row.fg_reference_id and production_item_wise_rm.get((row.fg_item, row.fg_reference_id)):
frappe.throw(_("Please set Parent Row No for item {0}").format(row.fg_item))
production_item_wise_rm[(row.fg_item, row.fg_reference_id)]["items"].append(row) production_item_wise_rm[(row.fg_item, row.fg_reference_id)]["items"].append(row)
reverse_tree = OrderedDict(reversed(list(production_item_wise_rm.items()))) reverse_tree = OrderedDict(reversed(list(production_item_wise_rm.items())))
@@ -283,7 +283,6 @@ class BOMCreator(Document):
"allow_alternative_item": 1, "allow_alternative_item": 1,
"bom_creator": self.name, "bom_creator": self.name,
"bom_creator_item": bom_creator_item, "bom_creator_item": bom_creator_item,
"rm_cost_as_per": "Manual",
} }
) )

View File

@@ -145,6 +145,7 @@ class Project(Document):
is_group=task_details.is_group, is_group=task_details.is_group,
color=task_details.color, color=task_details.color,
template_task=task_details.name, template_task=task_details.name,
priority=task_details.priority,
) )
).insert() ).insert()

View File

@@ -23,7 +23,11 @@ class TestProject(FrappeTestCase):
task1 = task_exists("Test Template Task with No Parent and Dependency") task1 = task_exists("Test Template Task with No Parent and Dependency")
if not task1: if not task1:
task1 = create_task( task1 = create_task(
subject="Test Template Task with No Parent and Dependency", is_template=1, begin=5, duration=3 subject="Test Template Task with No Parent and Dependency",
is_template=1,
begin=5,
duration=3,
priority="High",
) )
template = make_project_template( template = make_project_template(
@@ -32,11 +36,12 @@ class TestProject(FrappeTestCase):
project = get_project(project_name, template) project = get_project(project_name, template)
tasks = frappe.get_all( tasks = frappe.get_all(
"Task", "Task",
["subject", "exp_end_date", "depends_on_tasks"], ["subject", "exp_end_date", "depends_on_tasks", "priority"],
dict(project=project.name), dict(project=project.name),
order_by="creation asc", order_by="creation asc",
) )
self.assertEqual(tasks[0].priority, "High")
self.assertEqual(tasks[0].subject, "Test Template Task with No Parent and Dependency") self.assertEqual(tasks[0].subject, "Test Template Task with No Parent and Dependency")
self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 5, 3)) self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 5, 3))
self.assertEqual(len(tasks), 1) self.assertEqual(len(tasks), 1)

View File

@@ -122,6 +122,7 @@ def create_task(
begin=0, begin=0,
duration=0, duration=0,
save=True, save=True,
priority=None,
): ):
if not frappe.db.exists("Task", subject): if not frappe.db.exists("Task", subject):
task = frappe.new_doc("Task") task = frappe.new_doc("Task")
@@ -139,6 +140,7 @@ def create_task(
task.duration = duration task.duration = duration
task.is_group = is_group task.is_group = is_group
task.parent_task = parent_task task.parent_task = parent_task
task.priority = priority
if save: if save:
task.save() task.save()
else: else:

View File

@@ -342,7 +342,6 @@ erpnext.buying = {
add_serial_batch_bundle(doc, cdt, cdn) { add_serial_batch_bundle(doc, cdt, cdn) {
let item = locals[cdt][cdn]; let item = locals[cdt][cdn];
let me = this; let me = this;
let path = "assets/erpnext/js/utils/serial_no_batch_selector.js";
frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]) frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
.then((r) => { .then((r) => {
@@ -352,30 +351,28 @@ erpnext.buying = {
item.type_of_transaction = item.qty > 0 ? "Inward" : "Outward"; item.type_of_transaction = item.qty > 0 ? "Inward" : "Outward";
item.is_rejected = false; item.is_rejected = false;
frappe.require(path, function() { new erpnext.SerialBatchPackageSelector(
new erpnext.SerialBatchPackageSelector( me.frm, item, (r) => {
me.frm, item, (r) => { if (r) {
if (r) { let qty = Math.abs(r.total_qty);
let qty = Math.abs(r.total_qty); if (doc.is_return) {
if (doc.is_return) { qty = qty * -1;
qty = qty * -1;
}
let update_values = {
"serial_and_batch_bundle": r.name,
"use_serial_batch_fields": 0,
"qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item))
}
if (r.warehouse) {
update_values["warehouse"] = r.warehouse;
}
frappe.model.set_value(item.doctype, item.name, update_values);
} }
let update_values = {
"serial_and_batch_bundle": r.name,
"use_serial_batch_fields": 0,
"qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item))
}
if (r.warehouse) {
update_values["warehouse"] = r.warehouse;
}
frappe.model.set_value(item.doctype, item.name, update_values);
} }
); }
}); );
} }
}); });
} }
@@ -383,40 +380,37 @@ erpnext.buying = {
add_serial_batch_for_rejected_qty(doc, cdt, cdn) { add_serial_batch_for_rejected_qty(doc, cdt, cdn) {
let item = locals[cdt][cdn]; let item = locals[cdt][cdn];
let me = this; let me = this;
let path = "assets/erpnext/js/utils/serial_no_batch_selector.js";
frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]) frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
.then((r) => { .then((r) => {
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) { if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
item.has_serial_no = r.message.has_serial_no; item.has_serial_no = r.message.has_serial_no;
item.has_batch_no = r.message.has_batch_no; item.has_batch_no = r.message.has_batch_no;
item.type_of_transaction = item.qty > 0 ? "Inward" : "Outward"; item.type_of_transaction = item.rejected_qty > 0 ? "Inward" : "Outward";
item.is_rejected = true; item.is_rejected = true;
frappe.require(path, function() { new erpnext.SerialBatchPackageSelector(
new erpnext.SerialBatchPackageSelector( me.frm, item, (r) => {
me.frm, item, (r) => { if (r) {
if (r) { let qty = Math.abs(r.total_qty);
let qty = Math.abs(r.total_qty); if (doc.is_return) {
if (doc.is_return) { qty = qty * -1;
qty = qty * -1;
}
let update_values = {
"serial_and_batch_bundle": r.name,
"use_serial_batch_fields": 0,
"rejected_qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item))
}
if (r.warehouse) {
update_values["rejected_warehouse"] = r.warehouse;
}
frappe.model.set_value(item.doctype, item.name, update_values);
} }
let update_values = {
"serial_and_batch_bundle": r.name,
"use_serial_batch_fields": 0,
"rejected_qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item))
}
if (r.warehouse) {
update_values["rejected_warehouse"] = r.warehouse;
}
frappe.model.set_value(item.doctype, item.name, update_values);
} }
); }
}); );
} }
}); });
} }

View File

@@ -415,7 +415,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
let row = locals[cdt][cdn]; let row = locals[cdt][cdn];
if (row.barcode) { if (row.barcode) {
erpnext.stock.utils.set_item_details_using_barcode(this.frm, row, (r) => { erpnext.stock.utils.set_item_details_using_barcode(this.frm, row, (r) => {
debugger
frappe.model.set_value(cdt, cdn, { frappe.model.set_value(cdt, cdn, {
"item_code": r.message.item_code, "item_code": r.message.item_code,
"qty": 1, "qty": 1,
@@ -2499,27 +2498,25 @@ erpnext.show_serial_batch_selector = function (frm, item_row, callback, on_close
} }
} }
frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", function() { if (["Sales Invoice", "Delivery Note"].includes(frm.doc.doctype)) {
if (["Sales Invoice", "Delivery Note"].includes(frm.doc.doctype)) { item_row.type_of_transaction = frm.doc.is_return ? "Inward" : "Outward";
item_row.type_of_transaction = frm.doc.is_return ? "Inward" : "Outward"; } else {
} else { item_row.type_of_transaction = frm.doc.is_return ? "Outward" : "Inward";
item_row.type_of_transaction = frm.doc.is_return ? "Outward" : "Inward"; }
}
new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => { new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => {
if (r) { if (r) {
let update_values = { let update_values = {
"serial_and_batch_bundle": r.name, "serial_and_batch_bundle": r.name,
"qty": Math.abs(r.total_qty) "qty": Math.abs(r.total_qty)
}
if (r.warehouse) {
update_values[warehouse_field] = r.warehouse;
}
frappe.model.set_value(item_row.doctype, item_row.name, update_values);
} }
});
if (r.warehouse) {
update_values[warehouse_field] = r.warehouse;
}
frappe.model.set_value(item_row.doctype, item_row.name, update_values);
}
}); });
} }

View File

@@ -4,6 +4,7 @@ import "./queries";
import "./sms_manager"; import "./sms_manager";
import "./utils/party"; import "./utils/party";
import "./controllers/stock_controller"; import "./controllers/stock_controller";
import "./utils/serial_no_batch_selector";
import "./payment/payments"; import "./payment/payments";
import "./templates/visual_plant_floor_template.html"; import "./templates/visual_plant_floor_template.html";
import "./plant_floor_visual/visual_plant"; import "./plant_floor_visual/visual_plant";

View File

@@ -430,25 +430,23 @@ $.extend(erpnext.utils, {
item_row.has_batch_no = r.message.has_batch_no; item_row.has_batch_no = r.message.has_batch_no;
item_row.has_serial_no = r.message.has_serial_no; item_row.has_serial_no = r.message.has_serial_no;
frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", function () { new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => {
new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => { if (r) {
if (r) { let update_values = {
let update_values = { serial_and_batch_bundle: r.name,
serial_and_batch_bundle: r.name, qty: Math.abs(r.total_qty),
qty: Math.abs(r.total_qty), };
};
if (!warehouse_field) { if (!warehouse_field) {
warehouse_field = "warehouse"; warehouse_field = "warehouse";
}
if (r.warehouse) {
update_values[warehouse_field] = r.warehouse;
}
frappe.model.set_value(item_row.doctype, item_row.name, update_values);
} }
});
if (r.warehouse) {
update_values[warehouse_field] = r.warehouse;
}
frappe.model.set_value(item_row.doctype, item_row.name, update_values);
}
}); });
}); });
}, },

View File

@@ -350,7 +350,6 @@ erpnext.sales_common = {
pick_serial_and_batch(doc, cdt, cdn) { pick_serial_and_batch(doc, cdt, cdn) {
let item = locals[cdt][cdn]; let item = locals[cdt][cdn];
let me = this; let me = this;
let path = "assets/erpnext/js/utils/serial_no_batch_selector.js";
frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]).then((r) => { frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]).then((r) => {
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) { if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
@@ -364,26 +363,24 @@ erpnext.sales_common = {
item.title = __("Select Serial and Batch"); item.title = __("Select Serial and Batch");
} }
frappe.require(path, function () { new erpnext.SerialBatchPackageSelector(me.frm, item, (r) => {
new erpnext.SerialBatchPackageSelector(me.frm, item, (r) => { if (r) {
if (r) { let qty = Math.abs(r.total_qty);
let qty = Math.abs(r.total_qty); if (doc.is_return) {
if (doc.is_return) { qty = qty * -1;
qty = qty * -1;
}
frappe.model.set_value(item.doctype, item.name, {
serial_and_batch_bundle: r.name,
use_serial_batch_fields: 0,
qty:
qty /
flt(
item.conversion_factor || 1,
precision("conversion_factor", item)
),
});
} }
});
frappe.model.set_value(item.doctype, item.name, {
serial_and_batch_bundle: r.name,
use_serial_batch_fields: 0,
qty:
qty /
flt(
item.conversion_factor || 1,
precision("conversion_factor", item)
),
});
}
}); });
} }
}); });

View File

@@ -394,19 +394,17 @@ erpnext.PointOfSale.ItemDetails = class {
bind_auto_serial_fetch_event() { bind_auto_serial_fetch_event() {
this.$form_container.on("click", ".auto-fetch-btn", () => { this.$form_container.on("click", ".auto-fetch-btn", () => {
frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", () => { let frm = this.events.get_frm();
let frm = this.events.get_frm(); let item_row = this.item_row;
let item_row = this.item_row; item_row.type_of_transaction = "Outward";
item_row.type_of_transaction = "Outward";
new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => { new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => {
if (r) { if (r) {
frappe.model.set_value(item_row.doctype, item_row.name, { frappe.model.set_value(item_row.doctype, item_row.name, {
serial_and_batch_bundle: r.name, serial_and_batch_bundle: r.name,
qty: Math.abs(r.total_qty), qty: Math.abs(r.total_qty),
}); });
} }
});
}); });
}); });
} }

View File

@@ -1287,6 +1287,9 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
for tax in get_taxes_and_charges(master_doctype, target.get("taxes_and_charges")): for tax in get_taxes_and_charges(master_doctype, target.get("taxes_and_charges")):
target.append("taxes", tax) target.append("taxes", tax)
if not target.get("items"):
frappe.throw(_("All items have already been received"))
def update_details(source_doc, target_doc, source_parent): def update_details(source_doc, target_doc, source_parent):
target_doc.inter_company_invoice_reference = source_doc.name target_doc.inter_company_invoice_reference = source_doc.name
if target_doc.doctype == "Purchase Receipt": if target_doc.doctype == "Purchase Receipt":
@@ -1342,6 +1345,10 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
shipping_address_name=target_doc.shipping_address_name, shipping_address_name=target_doc.shipping_address_name,
) )
def update_item(source, target, source_parent):
if source_parent.doctype == "Delivery Note" and source.received_qty:
target.qty = flt(source.qty) + flt(source.returned_qty) - flt(source.received_qty)
doclist = get_mapped_doc( doclist = get_mapped_doc(
doctype, doctype,
source_name, source_name,
@@ -1363,6 +1370,8 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
"Material_request_item": "material_request_item", "Material_request_item": "material_request_item",
}, },
"field_no_map": ["warehouse"], "field_no_map": ["warehouse"],
"condition": lambda item: item.received_qty < item.qty + item.returned_qty,
"postprocess": update_item,
}, },
}, },
target_doc, target_doc,

View File

@@ -32,7 +32,7 @@ test_ignore = ["BOM"]
test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand", "Item Attribute"] test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand", "Item Attribute"]
def make_item(item_code=None, properties=None, uoms=None): def make_item(item_code=None, properties=None, uoms=None, barcode=None):
if not item_code: if not item_code:
item_code = frappe.generate_hash(length=16) item_code = frappe.generate_hash(length=16)
@@ -61,6 +61,14 @@ def make_item(item_code=None, properties=None, uoms=None):
for uom in uoms: for uom in uoms:
item.append("uoms", uom) item.append("uoms", uom)
if barcode:
item.append(
"barcodes",
{
"barcode": barcode,
},
)
item.insert() item.insert()
return item return item

View File

@@ -355,19 +355,15 @@ frappe.ui.form.on("Pick List Item", {
item.title = __("Select Serial and Batch"); item.title = __("Select Serial and Batch");
} }
frappe.require(path, function () { new erpnext.SerialBatchPackageSelector(frm, item, (r) => {
new erpnext.SerialBatchPackageSelector(frm, item, (r) => { if (r) {
if (r) { let qty = Math.abs(r.total_qty);
let qty = Math.abs(r.total_qty); frappe.model.set_value(item.doctype, item.name, {
frappe.model.set_value(item.doctype, item.name, { serial_and_batch_bundle: r.name,
serial_and_batch_bundle: r.name, use_serial_batch_fields: 0,
use_serial_batch_fields: 0, qty: qty / flt(item.conversion_factor || 1, precision("conversion_factor", item)),
qty: });
qty / }
flt(item.conversion_factor || 1, precision("conversion_factor", item)),
});
}
});
}); });
} }
}); });

View File

@@ -18,6 +18,7 @@
"parent_warehouse", "parent_warehouse",
"consider_rejected_warehouses", "consider_rejected_warehouses",
"get_item_locations", "get_item_locations",
"pick_manually",
"section_break_6", "section_break_6",
"scan_barcode", "scan_barcode",
"column_break_13", "column_break_13",
@@ -192,11 +193,18 @@
"fieldname": "consider_rejected_warehouses", "fieldname": "consider_rejected_warehouses",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Consider Rejected Warehouses" "label": "Consider Rejected Warehouses"
},
{
"default": "0",
"description": "If enabled then system won't override the picked qty / batches / serial numbers.",
"fieldname": "pick_manually",
"fieldtype": "Check",
"label": "Pick Manually"
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2024-02-02 16:17:44.877426", "modified": "2024-03-27 22:49:16.954637",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Pick List", "name": "Pick List",

View File

@@ -42,6 +42,7 @@ class PickList(Document):
amended_from: DF.Link | None amended_from: DF.Link | None
company: DF.Link company: DF.Link
consider_rejected_warehouses: DF.Check
customer: DF.Link | None customer: DF.Link | None
customer_name: DF.Data | None customer_name: DF.Data | None
for_qty: DF.Float for_qty: DF.Float
@@ -50,6 +51,7 @@ class PickList(Document):
material_request: DF.Link | None material_request: DF.Link | None
naming_series: DF.Literal["STO-PICK-.YYYY.-"] naming_series: DF.Literal["STO-PICK-.YYYY.-"]
parent_warehouse: DF.Link | None parent_warehouse: DF.Link | None
pick_manually: DF.Check
prompt_qty: DF.Check prompt_qty: DF.Check
purpose: DF.Literal["Material Transfer for Manufacture", "Material Transfer", "Delivery"] purpose: DF.Literal["Material Transfer for Manufacture", "Material Transfer", "Delivery"]
scan_barcode: DF.Data | None scan_barcode: DF.Data | None
@@ -71,7 +73,8 @@ class PickList(Document):
def before_save(self): def before_save(self):
self.update_status() self.update_status()
self.set_item_locations() if not self.pick_manually:
self.set_item_locations()
if self.get("locations"): if self.get("locations"):
self.validate_sales_order_percentage() self.validate_sales_order_percentage()

View File

@@ -1335,18 +1335,16 @@ erpnext.stock.select_batch_and_serial_no = (frm, item) => {
item.has_batch_no = r.message.has_batch_no; item.has_batch_no = r.message.has_batch_no;
item.type_of_transaction = item.s_warehouse ? "Outward" : "Inward"; item.type_of_transaction = item.s_warehouse ? "Outward" : "Inward";
frappe.require(path, function () { new erpnext.SerialBatchPackageSelector(frm, item, (r) => {
new erpnext.SerialBatchPackageSelector(frm, item, (r) => { if (r) {
if (r) { frappe.model.set_value(item.doctype, item.name, {
frappe.model.set_value(item.doctype, item.name, { serial_and_batch_bundle: r.name,
serial_and_batch_bundle: r.name, use_serial_batch_fields: 0,
use_serial_batch_fields: 0, qty:
qty: Math.abs(r.total_qty) /
Math.abs(r.total_qty) / flt(item.conversion_factor || 1, precision("conversion_factor", item)),
flt(item.conversion_factor || 1, precision("conversion_factor", item)), });
}); }
}
});
}); });
} }
}); });

View File

@@ -31,6 +31,7 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
OpeningEntryAccountError, OpeningEntryAccountError,
) )
from erpnext.stock.get_item_details import ( from erpnext.stock.get_item_details import (
get_barcode_data,
get_bin_details, get_bin_details,
get_conversion_factor, get_conversion_factor,
get_default_cost_center, get_default_cost_center,
@@ -428,7 +429,14 @@ class StockEntry(StockController):
for field in reset_fields: for field in reset_fields:
item.set(field, item_details.get(field)) item.set(field, item_details.get(field))
update_fields = ("uom", "description", "expense_account", "cost_center", "conversion_factor") update_fields = (
"uom",
"description",
"expense_account",
"cost_center",
"conversion_factor",
"barcode",
)
for field in update_fields: for field in update_fields:
if not item.get(field): if not item.get(field):
@@ -1609,6 +1617,10 @@ class StockEntry(StockController):
if subcontract_items and len(subcontract_items) == 1: if subcontract_items and len(subcontract_items) == 1:
ret["subcontracted_item"] = subcontract_items[0].main_item_code ret["subcontracted_item"] = subcontract_items[0].main_item_code
barcode_data = get_barcode_data(item_code=item.name)
if barcode_data and len(barcode_data.get(item.name)) == 1:
ret["barcode"] = barcode_data.get(item.name)[0]
return ret return ret
@frappe.whitelist() @frappe.whitelist()

View File

@@ -98,6 +98,12 @@ class TestStockEntry(FrappeTestCase):
self._test_auto_material_request("_Test Item") self._test_auto_material_request("_Test Item")
self._test_auto_material_request("_Test Item", material_request_type="Transfer") self._test_auto_material_request("_Test Item", material_request_type="Transfer")
def test_barcode_item_stock_entry(self):
item_code = make_item("_Test Item Stock Entry For Barcode", barcode="BDD-1234567890")
se = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100)
self.assertEqual(se.items[0].barcode, "BDD-1234567890")
def test_auto_material_request_for_variant(self): def test_auto_material_request_for_variant(self):
fields = [{"field_name": "reorder_levels"}] fields = [{"field_name": "reorder_levels"}]
set_item_variant_settings(fields) set_item_variant_settings(fields)

View File

@@ -81,6 +81,18 @@ frappe.ui.form.on("Stock Reconciliation", {
if (frm.doc.company) { if (frm.doc.company) {
frm.trigger("toggle_display_account_head"); frm.trigger("toggle_display_account_head");
} }
frm.events.set_fields_onload_for_line_item(frm);
},
set_fields_onload_for_line_item(frm) {
if (frm.is_new() && frm.doc?.items && cint(frappe.user_defaults?.use_serial_batch_fields) === 1) {
frm.doc.items.forEach((item) => {
if (!item.serial_and_batch_bundle) {
frappe.model.set_value(item.doctype, item.name, "use_serial_batch_fields", 1);
}
});
}
}, },
scan_barcode: function (frm) { scan_barcode: function (frm) {
@@ -155,6 +167,9 @@ frappe.ui.form.on("Stock Reconciliation", {
item.qty = item.qty || 0; item.qty = item.qty || 0;
item.valuation_rate = item.valuation_rate || 0; item.valuation_rate = item.valuation_rate || 0;
item.use_serial_batch_fields = cint(
frappe.user_defaults?.use_serial_batch_fields
);
}); });
frm.refresh_field("items"); frm.refresh_field("items");
}, },
@@ -298,6 +313,10 @@ frappe.ui.form.on("Stock Reconciliation Item", {
if (!item.warehouse && frm.doc.set_warehouse) { if (!item.warehouse && frm.doc.set_warehouse) {
frappe.model.set_value(cdt, cdn, "warehouse", frm.doc.set_warehouse); frappe.model.set_value(cdt, cdn, "warehouse", frm.doc.set_warehouse);
} }
if (item.docstatus === 0 && cint(frappe.user_defaults?.use_serial_batch_fields) === 1) {
frappe.model.set_value(item.doctype, item.name, "use_serial_batch_fields", 1);
}
}, },
add_serial_batch_bundle(frm, cdt, cdn) { add_serial_batch_bundle(frm, cdt, cdn) {

View File

@@ -49,6 +49,7 @@ frappe.ui.form.on("Warehouse", {
frm.add_custom_button(__("Stock Balance"), function () { frm.add_custom_button(__("Stock Balance"), function () {
frappe.set_route("query-report", "Stock Balance", { frappe.set_route("query-report", "Stock Balance", {
warehouse: frm.doc.name, warehouse: frm.doc.name,
company: frm.doc.company,
}); });
}); });

View File

@@ -499,12 +499,21 @@ def update_barcode_value(out):
out["barcode"] = barcode_data.get(out.item_code)[0] out["barcode"] = barcode_data.get(out.item_code)[0]
def get_barcode_data(items_list): def get_barcode_data(items_list=None, item_code=None):
# get item-wise batch no data # get item-wise batch no data
# example: {'LED-GRE': [Batch001, Batch002]} # example: {'LED-GRE': [Batch001, Batch002]}
# where LED-GRE is item code, SN0001 is serial no and Pune is warehouse # where LED-GRE is item code, SN0001 is serial no and Pune is warehouse
itemwise_barcode = {} itemwise_barcode = {}
if not items_list and item_code:
_dict_item_code = frappe._dict(
{
"item_code": item_code,
}
)
items_list = [frappe._dict(_dict_item_code)]
for item in items_list: for item in items_list:
barcodes = frappe.db.get_all( barcodes = frappe.db.get_all(
"Item Barcode", filters={"parent": item.item_code}, fields="barcode" "Item Barcode", filters={"parent": item.item_code}, fields="barcode"

View File

@@ -2,8 +2,8 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from collections.abc import Iterator
from operator import itemgetter from operator import itemgetter
from typing import Dict, List, Tuple, Union
import frappe import frappe
from frappe import _ from frappe import _
@@ -14,7 +14,7 @@ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
Filters = frappe._dict Filters = frappe._dict
def execute(filters: Filters = None) -> Tuple: def execute(filters: Filters = None) -> tuple:
to_date = filters["to_date"] to_date = filters["to_date"]
columns = get_columns(filters) columns = get_columns(filters)
@@ -26,14 +26,14 @@ def execute(filters: Filters = None) -> Tuple:
return columns, data, None, chart_data return columns, data, None, chart_data
def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> List[Dict]: def format_report_data(filters: Filters, item_details: dict, to_date: str) -> list[dict]:
"Returns ordered, formatted data with ranges." "Returns ordered, formatted data with ranges."
_func = itemgetter(1) _func = itemgetter(1)
data = [] data = []
precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True)) precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True))
for item, item_dict in item_details.items(): for _item, item_dict in item_details.items():
if not flt(item_dict.get("total_qty"), precision): if not flt(item_dict.get("total_qty"), precision):
continue continue
@@ -74,12 +74,12 @@ def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> Li
return data return data
def get_average_age(fifo_queue: List, to_date: str) -> float: def get_average_age(fifo_queue: list, to_date: str) -> float:
batch_age = age_qty = total_qty = 0.0 batch_age = age_qty = total_qty = 0.0
for batch in fifo_queue: for batch in fifo_queue:
batch_age = date_diff(to_date, batch[1]) batch_age = date_diff(to_date, batch[1])
if isinstance(batch[0], (int, float)): if isinstance(batch[0], int | float):
age_qty += batch_age * batch[0] age_qty += batch_age * batch[0]
total_qty += batch[0] total_qty += batch[0]
else: else:
@@ -89,8 +89,7 @@ def get_average_age(fifo_queue: List, to_date: str) -> float:
return flt(age_qty / total_qty, 2) if total_qty else 0.0 return flt(age_qty / total_qty, 2) if total_qty else 0.0
def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: Dict) -> Tuple: def get_range_age(filters: Filters, fifo_queue: list, to_date: str, item_dict: dict) -> tuple:
precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True)) precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True))
range1 = range2 = range3 = above_range3 = 0.0 range1 = range2 = range3 = above_range3 = 0.0
@@ -111,7 +110,7 @@ def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: D
return range1, range2, range3, above_range3 return range1, range2, range3, above_range3
def get_columns(filters: Filters) -> List[Dict]: def get_columns(filters: Filters) -> list[dict]:
range_columns = [] range_columns = []
setup_ageing_columns(filters, range_columns) setup_ageing_columns(filters, range_columns)
columns = [ columns = [
@@ -169,7 +168,7 @@ def get_columns(filters: Filters) -> List[Dict]:
return columns return columns
def get_chart_data(data: List, filters: Filters) -> Dict: def get_chart_data(data: list, filters: Filters) -> dict:
if not data: if not data:
return [] return []
@@ -193,7 +192,7 @@ def get_chart_data(data: List, filters: Filters) -> Dict:
} }
def setup_ageing_columns(filters: Filters, range_columns: List): def setup_ageing_columns(filters: Filters, range_columns: list):
ranges = [ ranges = [
f"0 - {filters['range1']}", f"0 - {filters['range1']}",
f"{cint(filters['range1']) + 1} - {cint(filters['range2'])}", f"{cint(filters['range1']) + 1} - {cint(filters['range2'])}",
@@ -205,23 +204,21 @@ def setup_ageing_columns(filters: Filters, range_columns: List):
add_column(range_columns, label=_("Age ({0})").format(label), fieldname=fieldname) add_column(range_columns, label=_("Age ({0})").format(label), fieldname=fieldname)
def add_column( def add_column(range_columns: list, label: str, fieldname: str, fieldtype: str = "Float", width: int = 140):
range_columns: List, label: str, fieldname: str, fieldtype: str = "Float", width: int = 140
):
range_columns.append(dict(label=label, fieldname=fieldname, fieldtype=fieldtype, width=width)) range_columns.append(dict(label=label, fieldname=fieldname, fieldtype=fieldtype, width=width))
class FIFOSlots: class FIFOSlots:
"Returns FIFO computed slots of inwarded stock as per date." "Returns FIFO computed slots of inwarded stock as per date."
def __init__(self, filters: Dict = None, sle: List = None): def __init__(self, filters: dict | None = None, sle: list | None = None):
self.item_details = {} self.item_details = {}
self.transferred_item_details = {} self.transferred_item_details = {}
self.serial_no_batch_purchase_details = {} self.serial_no_batch_purchase_details = {}
self.filters = filters self.filters = filters
self.sle = sle self.sle = sle
def generate(self) -> Dict: def generate(self) -> dict:
""" """
Returns dict of the foll.g structure: Returns dict of the foll.g structure:
Key = Item A / (Item A, Warehouse A) Key = Item A / (Item A, Warehouse A)
@@ -231,25 +228,45 @@ class FIFOSlots:
consumed/updated and maintained via FIFO. ** consumed/updated and maintained via FIFO. **
} }
""" """
if self.sle is None:
self.sle = self.__get_stock_ledger_entries()
for d in self.sle: from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
key, fifo_queue, transferred_item_key = self.__init_key_stores(d) get_serial_nos_from_bundle,
)
if d.voucher_type == "Stock Reconciliation": stock_ledger_entries = self.sle
# get difference in qty shift as actual qty
prev_balance_qty = self.item_details[key].get("qty_after_transaction", 0)
d.actual_qty = flt(d.qty_after_transaction) - flt(prev_balance_qty)
serial_nos = get_serial_nos(d.serial_no) if d.serial_no else [] bundle_wise_serial_nos = frappe._dict({})
if stock_ledger_entries is None:
bundle_wise_serial_nos = self.__get_bundle_wise_serial_nos()
if d.actual_qty > 0: with frappe.db.unbuffered_cursor():
self.__compute_incoming_stock(d, fifo_queue, transferred_item_key, serial_nos) if stock_ledger_entries is None:
else: stock_ledger_entries = self.__get_stock_ledger_entries()
self.__compute_outgoing_stock(d, fifo_queue, transferred_item_key, serial_nos)
self.__update_balances(d, key) for d in stock_ledger_entries:
key, fifo_queue, transferred_item_key = self.__init_key_stores(d)
if d.voucher_type == "Stock Reconciliation":
# get difference in qty shift as actual qty
prev_balance_qty = self.item_details[key].get("qty_after_transaction", 0)
d.actual_qty = flt(d.qty_after_transaction) - flt(prev_balance_qty)
serial_nos = get_serial_nos(d.serial_no) if d.serial_no else []
if d.serial_and_batch_bundle and d.has_serial_no:
if bundle_wise_serial_nos:
serial_nos = bundle_wise_serial_nos.get(d.serial_and_batch_bundle) or []
else:
serial_nos = get_serial_nos_from_bundle(d.serial_and_batch_bundle) or []
if d.actual_qty > 0:
self.__compute_incoming_stock(d, fifo_queue, transferred_item_key, serial_nos)
else:
self.__compute_outgoing_stock(d, fifo_queue, transferred_item_key, serial_nos)
self.__update_balances(d, key)
# Note that stock_ledger_entries is an iterator, you can not reuse it like a list
del stock_ledger_entries
if not self.filters.get("show_warehouse_wise_stock"): if not self.filters.get("show_warehouse_wise_stock"):
# (Item 1, WH 1), (Item 1, WH 2) => (Item 1) # (Item 1, WH 1), (Item 1, WH 2) => (Item 1)
@@ -257,7 +274,7 @@ class FIFOSlots:
return self.item_details return self.item_details
def __init_key_stores(self, row: Dict) -> Tuple: def __init_key_stores(self, row: dict) -> tuple:
"Initialise keys and FIFO Queue." "Initialise keys and FIFO Queue."
key = (row.name, row.warehouse) key = (row.name, row.warehouse)
@@ -269,9 +286,7 @@ class FIFOSlots:
return key, fifo_queue, transferred_item_key return key, fifo_queue, transferred_item_key
def __compute_incoming_stock( def __compute_incoming_stock(self, row: dict, fifo_queue: list, transfer_key: tuple, serial_nos: list):
self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List
):
"Update FIFO Queue on inward stock." "Update FIFO Queue on inward stock."
transfer_data = self.transferred_item_details.get(transfer_key) transfer_data = self.transferred_item_details.get(transfer_key)
@@ -297,9 +312,7 @@ class FIFOSlots:
self.serial_no_batch_purchase_details.setdefault(serial_no, row.posting_date) self.serial_no_batch_purchase_details.setdefault(serial_no, row.posting_date)
fifo_queue.append([serial_no, row.posting_date]) fifo_queue.append([serial_no, row.posting_date])
def __compute_outgoing_stock( def __compute_outgoing_stock(self, row: dict, fifo_queue: list, transfer_key: tuple, serial_nos: list):
self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List
):
"Update FIFO Queue on outward stock." "Update FIFO Queue on outward stock."
if serial_nos: if serial_nos:
fifo_queue[:] = [serial_no for serial_no in fifo_queue if serial_no[0] not in serial_nos] fifo_queue[:] = [serial_no for serial_no in fifo_queue if serial_no[0] not in serial_nos]
@@ -325,7 +338,7 @@ class FIFOSlots:
self.transferred_item_details[transfer_key].append([qty_to_pop, slot[1]]) self.transferred_item_details[transfer_key].append([qty_to_pop, slot[1]])
qty_to_pop = 0 qty_to_pop = 0
def __adjust_incoming_transfer_qty(self, transfer_data: Dict, fifo_queue: List, row: Dict): def __adjust_incoming_transfer_qty(self, transfer_data: dict, fifo_queue: list, row: dict):
"Add previously removed stock back to FIFO Queue." "Add previously removed stock back to FIFO Queue."
transfer_qty_to_pop = flt(row.actual_qty) transfer_qty_to_pop = flt(row.actual_qty)
@@ -352,7 +365,7 @@ class FIFOSlots:
add_to_fifo_queue([transfer_qty_to_pop, transfer_data[0][1]]) add_to_fifo_queue([transfer_qty_to_pop, transfer_data[0][1]])
transfer_qty_to_pop = 0 transfer_qty_to_pop = 0
def __update_balances(self, row: Dict, key: Union[Tuple, str]): def __update_balances(self, row: dict, key: tuple | str):
self.item_details[key]["qty_after_transaction"] = row.qty_after_transaction self.item_details[key]["qty_after_transaction"] = row.qty_after_transaction
if "total_qty" not in self.item_details[key]: if "total_qty" not in self.item_details[key]:
@@ -362,7 +375,7 @@ class FIFOSlots:
self.item_details[key]["has_serial_no"] = row.has_serial_no self.item_details[key]["has_serial_no"] = row.has_serial_no
def __aggregate_details_by_item(self, wh_wise_data: Dict) -> Dict: def __aggregate_details_by_item(self, wh_wise_data: dict) -> dict:
"Aggregate Item-Wh wise data into single Item entry." "Aggregate Item-Wh wise data into single Item entry."
item_aggregated_data = {} item_aggregated_data = {}
for key, row in wh_wise_data.items(): for key, row in wh_wise_data.items():
@@ -370,7 +383,12 @@ class FIFOSlots:
if not item_aggregated_data.get(item): if not item_aggregated_data.get(item):
item_aggregated_data.setdefault( item_aggregated_data.setdefault(
item, item,
{"details": frappe._dict(), "fifo_queue": [], "qty_after_transaction": 0.0, "total_qty": 0.0}, {
"details": frappe._dict(),
"fifo_queue": [],
"qty_after_transaction": 0.0,
"total_qty": 0.0,
},
) )
item_row = item_aggregated_data.get(item) item_row = item_aggregated_data.get(item)
item_row["details"].update(row["details"]) item_row["details"].update(row["details"])
@@ -381,7 +399,7 @@ class FIFOSlots:
return item_aggregated_data return item_aggregated_data
def __get_stock_ledger_entries(self) -> List[Dict]: def __get_stock_ledger_entries(self) -> Iterator[dict]:
sle = frappe.qb.DocType("Stock Ledger Entry") sle = frappe.qb.DocType("Stock Ledger Entry")
item = self.__get_item_query() # used as derived table in sle query item = self.__get_item_query() # used as derived table in sle query
@@ -403,6 +421,7 @@ class FIFOSlots:
sle.serial_no, sle.serial_no,
sle.batch_no, sle.batch_no,
sle.qty_after_transaction, sle.qty_after_transaction,
sle.serial_and_batch_bundle,
sle.warehouse, sle.warehouse,
) )
.where( .where(
@@ -418,7 +437,34 @@ class FIFOSlots:
sle_query = sle_query.orderby(sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty) sle_query = sle_query.orderby(sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty)
return sle_query.run(as_dict=True) return sle_query.run(as_dict=True, as_iterator=True)
def __get_bundle_wise_serial_nos(self) -> dict:
bundle = frappe.qb.DocType("Serial and Batch Bundle")
entry = frappe.qb.DocType("Serial and Batch Entry")
query = (
frappe.qb.from_(bundle)
.join(entry)
.on(bundle.name == entry.parent)
.select(bundle.name, entry.serial_no)
.where(
(bundle.docstatus == 1)
& (entry.serial_no.isnotnull())
& (bundle.company == self.filters.get("company"))
& (bundle.posting_date <= self.filters.get("to_date"))
)
)
for field in ["item_code", "warehouse"]:
if self.filters.get(field):
query = query.where(bundle[field] == self.filters.get(field))
bundle_wise_serial_nos = frappe._dict({})
for bundle_name, serial_no in query.run():
bundle_wise_serial_nos.setdefault(bundle_name, []).append(serial_no)
return bundle_wise_serial_nos
def __get_item_query(self) -> str: def __get_item_query(self) -> str:
item_table = frappe.qb.DocType("Item") item_table = frappe.qb.DocType("Item")

View File

@@ -295,6 +295,8 @@ class StockBalanceReport(object):
sle.stock_value, sle.stock_value,
sle.batch_no, sle.batch_no,
sle.serial_no, sle.serial_no,
sle.serial_and_batch_bundle,
sle.has_serial_no,
item_table.item_group, item_table.item_group,
item_table.stock_uom, item_table.stock_uom,
item_table.item_name, item_table.item_name,

View File

@@ -856,6 +856,11 @@ class SerialBatchCreation:
if not doc.get("entries"): if not doc.get("entries"):
return frappe._dict({}) return frappe._dict({})
if (
doc.voucher_no and frappe.get_cached_value(doc.voucher_type, doc.voucher_no, "docstatus") == 2
):
doc.voucher_no = ""
doc.save() doc.save()
self.validate_qty(doc) self.validate_qty(doc)

View File

@@ -335,12 +335,115 @@ frappe.ui.form.on("Subcontracting Receipt Item", {
items_remove: (frm) => { items_remove: (frm) => {
set_missing_values(frm); set_missing_values(frm);
}, },
add_serial_batch_bundle(frm, cdt, cdn) {
let item = locals[cdt][cdn];
frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]).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;
item.type_of_transaction = item.qty > 0 ? "Inward" : "Outward";
item.is_rejected = false;
new erpnext.SerialBatchPackageSelector(frm, item, (r) => {
if (r) {
let qty = Math.abs(r.total_qty);
if (frm.doc.is_return) {
qty = qty * -1;
}
let update_values = {
serial_and_batch_bundle: r.name,
use_serial_batch_fields: 0,
qty: qty / flt(item.conversion_factor || 1, precision("conversion_factor", item)),
};
if (r.warehouse) {
update_values["warehouse"] = r.warehouse;
}
frappe.model.set_value(item.doctype, item.name, update_values);
}
});
}
});
},
add_serial_batch_for_rejected_qty(frm, cdt, cdn) {
let item = locals[cdt][cdn];
frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]).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;
item.type_of_transaction = item.rejected_qty > 0 ? "Inward" : "Outward";
item.is_rejected = true;
new erpnext.SerialBatchPackageSelector(frm, item, (r) => {
if (r) {
let qty = Math.abs(r.total_qty);
if (frm.doc.is_return) {
qty = qty * -1;
}
let update_values = {
serial_and_batch_bundle: r.name,
use_serial_batch_fields: 0,
rejected_qty:
qty / flt(item.conversion_factor || 1, precision("conversion_factor", item)),
};
if (r.warehouse) {
update_values["rejected_warehouse"] = r.warehouse;
}
frappe.model.set_value(item.doctype, item.name, update_values);
}
});
}
});
},
}); });
frappe.ui.form.on("Subcontracting Receipt Supplied Item", { frappe.ui.form.on("Subcontracting Receipt Supplied Item", {
consumed_qty(frm) { consumed_qty(frm) {
set_missing_values(frm); set_missing_values(frm);
}, },
add_serial_batch_bundle(frm, cdt, cdn) {
let item = locals[cdt][cdn];
item.item_code = item.rm_item_code;
item.qty = item.consumed_qty;
item.warehouse = frm.doc.supplier_warehouse;
frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]).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;
item.type_of_transaction = item.qty > 0 ? "Outward" : "Inward";
item.is_rejected = false;
new erpnext.SerialBatchPackageSelector(frm, item, (r) => {
if (r) {
let qty = Math.abs(r.total_qty);
if (frm.doc.is_return) {
qty = qty * -1;
}
let update_values = {
serial_and_batch_bundle: r.name,
use_serial_batch_fields: 0,
consumed_qty:
qty / flt(item.conversion_factor || 1, precision("conversion_factor", item)),
};
frappe.model.set_value(item.doctype, item.name, update_values);
}
});
}
});
},
}); });
let set_warehouse_in_children = (child_table, warehouse_field, warehouse) => { let set_warehouse_in_children = (child_table, warehouse_field, warehouse) => {

View File

@@ -47,9 +47,11 @@
"schedule_date", "schedule_date",
"reference_name", "reference_name",
"section_break_45", "section_break_45",
"add_serial_batch_bundle",
"serial_and_batch_bundle", "serial_and_batch_bundle",
"use_serial_batch_fields", "use_serial_batch_fields",
"col_break5", "col_break5",
"add_serial_batch_for_rejected_qty",
"rejected_serial_and_batch_bundle", "rejected_serial_and_batch_bundle",
"section_break_jshh", "section_break_jshh",
"serial_no", "serial_no",
@@ -563,12 +565,24 @@
{ {
"fieldname": "column_break_henr", "fieldname": "column_break_henr",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 0",
"fieldname": "add_serial_batch_bundle",
"fieldtype": "Button",
"label": "Add Serial / Batch Bundle"
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 0",
"fieldname": "add_serial_batch_for_rejected_qty",
"fieldtype": "Button",
"label": "Add Serial / Batch No (Rejected Qty)"
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-03-07 11:43:38.954262", "modified": "2024-03-29 15:42:43.425544",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Subcontracting", "module": "Subcontracting",
"name": "Subcontracting Receipt Item", "name": "Subcontracting Receipt Item",
@@ -579,4 +593,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [] "states": []
} }

View File

@@ -25,6 +25,7 @@
"consumed_qty", "consumed_qty",
"current_stock", "current_stock",
"secbreak_3", "secbreak_3",
"add_serial_batch_bundle",
"serial_and_batch_bundle", "serial_and_batch_bundle",
"use_serial_batch_fields", "use_serial_batch_fields",
"col_break4", "col_break4",
@@ -224,12 +225,18 @@
{ {
"fieldname": "column_break_qibi", "fieldname": "column_break_qibi",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 0",
"fieldname": "add_serial_batch_bundle",
"fieldtype": "Button",
"label": "Add Serial / Batch Bundle"
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-02-04 16:32:17.534162", "modified": "2024-03-30 10:26:27.237371",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Subcontracting", "module": "Subcontracting",
"name": "Subcontracting Receipt Supplied Item", "name": "Subcontracting Receipt Supplied Item",
@@ -240,4 +247,4 @@
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -1,14 +1,14 @@
{ {
"accept_payment": 0,
"allow_comments": 1, "allow_comments": 1,
"allow_delete": 1, "allow_delete": 1,
"allow_edit": 1, "allow_edit": 1,
"allow_incomplete": 0, "allow_incomplete": 0,
"allow_multiple": 1, "allow_multiple": 1,
"allow_print": 0, "allow_print": 0,
"amount": 0.0, "anonymous": 0,
"amount_based_on_field": 0, "apply_document_permissions": 0,
"breadcrumbs": "[{\"label\":_(\"Issues\"), \"route\":\"issues\"}]", "breadcrumbs": "[{\"label\":_(\"Issues\"), \"route\":\"issues\"}]",
"condition_json": "[]",
"creation": "2016-06-24 15:50:33.186483", "creation": "2016-06-24 15:50:33.186483",
"doc_type": "Issue", "doc_type": "Issue",
"docstatus": 0, "docstatus": 0,
@@ -16,20 +16,19 @@
"idx": 0, "idx": 0,
"introduction_text": "", "introduction_text": "",
"is_standard": 1, "is_standard": 1,
"list_columns": [],
"login_required": 1, "login_required": 1,
"max_attachment_size": 0, "max_attachment_size": 0,
"modified": "2020-05-19 13:01:10.729088", "modified": "2024-03-27 16:16:03.621730",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Support", "module": "Support",
"name": "issues", "name": "issues",
"owner": "Administrator", "owner": "Administrator",
"published": 1, "published": 1,
"route": "issues", "route": "issues",
"route_to_success_link": 0,
"show_attachments": 0, "show_attachments": 0,
"show_in_grid": 0, "show_list": 1,
"show_sidebar": 1, "show_sidebar": 1,
"sidebar_items": [],
"success_message": "", "success_message": "",
"success_url": "/issues", "success_url": "/issues",
"title": "Issue", "title": "Issue",