mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-30 20:18:27 +00:00
Merge pull request #40820 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -453,7 +453,10 @@ frappe.ui.form.on("Journal Entry Account", {
|
||||
}
|
||||
},
|
||||
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) {
|
||||
|
||||
@@ -579,6 +579,7 @@
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"label": "Payment Order Status",
|
||||
"no_copy": 1,
|
||||
"options": "Initiated\nPayment Ordered",
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -821,4 +822,4 @@
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -671,7 +671,7 @@ class ReceivablePayableReport(object):
|
||||
else:
|
||||
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:
|
||||
row.future_amount = row.outstanding
|
||||
future[future_amount_field] = future.get(future_amount_field) - row.outstanding
|
||||
|
||||
@@ -469,11 +469,30 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
)
|
||||
|
||||
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()
|
||||
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.paid_amount = 90.0
|
||||
pe.references[0].allocated_amount = 90.0
|
||||
pe.paid_amount = 80
|
||||
pe.references[0].allocated_amount = 90.0 # pe.paid_amount + sr.grand_total
|
||||
pe.save().submit()
|
||||
filters = {
|
||||
"company": self.company,
|
||||
@@ -485,16 +504,21 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
"show_future_payments": True,
|
||||
}
|
||||
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]
|
||||
self.assertEqual(
|
||||
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
|
||||
)
|
||||
rows = report[:2]
|
||||
for row in rows:
|
||||
self.assertEqual(
|
||||
expected_data[row.voucher_no],
|
||||
[row.invoiced or row.paid, row.outstanding, row.remaining_balance, row.future_amount],
|
||||
)
|
||||
|
||||
pe.cancel()
|
||||
sr.load_from_db() # Outstanding amount is updated so a updated timestamp is needed.
|
||||
sr.cancel()
|
||||
|
||||
# full payment in future date
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.posting_date = add_days(today(), 1)
|
||||
|
||||
@@ -462,7 +462,7 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "other_charges_calculation",
|
||||
"fieldtype": "Markdown Editor",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Taxes and Charges Calculation",
|
||||
"no_copy": 1,
|
||||
"oldfieldtype": "HTML",
|
||||
@@ -928,7 +928,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-20 16:03:59.069145",
|
||||
"modified": "2024-03-28 10:20:30.231915",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier Quotation",
|
||||
|
||||
@@ -71,7 +71,7 @@ class SupplierQuotation(BuyingController):
|
||||
naming_series: DF.Literal["PUR-SQTN-.YYYY.-"]
|
||||
net_total: DF.Currency
|
||||
opportunity: DF.Link | None
|
||||
other_charges_calculation: DF.MarkdownEditor | None
|
||||
other_charges_calculation: DF.TextEditor | None
|
||||
plc_conversion_rate: DF.Float
|
||||
price_list_currency: DF.Link | None
|
||||
pricing_rules: DF.Table[PricingRuleDetail]
|
||||
|
||||
@@ -750,12 +750,12 @@ def get_serial_and_batch_bundle(child, parent):
|
||||
"item_code": child.item_code,
|
||||
"warehouse": child.warehouse,
|
||||
"voucher_type": parent.doctype,
|
||||
"voucher_no": parent.name,
|
||||
"voucher_no": parent.name if parent.docstatus < 2 else None,
|
||||
"voucher_detail_no": child.name,
|
||||
"posting_date": parent.posting_date,
|
||||
"posting_time": parent.posting_time,
|
||||
"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,
|
||||
"do_not_submit": "True",
|
||||
}
|
||||
|
||||
@@ -913,7 +913,7 @@ class StockController(AccountsController):
|
||||
self.validate_multi_currency()
|
||||
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()
|
||||
else:
|
||||
self.validate_internal_transfer_warehouse()
|
||||
|
||||
@@ -238,7 +238,7 @@
|
||||
"fieldname": "rm_cost_as_per",
|
||||
"fieldtype": "Select",
|
||||
"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,
|
||||
@@ -637,7 +637,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-26 19:34:08.159312",
|
||||
"modified": "2024-04-02 16:22:47.518411",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM",
|
||||
@@ -676,4 +676,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ class BOM(WebsiteGenerator):
|
||||
quality_inspection_template: DF.Link | None
|
||||
quantity: DF.Float
|
||||
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
|
||||
routing: DF.Link | None
|
||||
scrap_items: DF.Table[BOMScrapItem]
|
||||
@@ -742,6 +742,7 @@ class BOM(WebsiteGenerator):
|
||||
|
||||
def calculate_rm_cost(self, save=False):
|
||||
"""Fetch RM rate as per today's valuation rate and calculate totals"""
|
||||
|
||||
total_rm_cost = 0
|
||||
base_total_rm_cost = 0
|
||||
|
||||
@@ -750,7 +751,7 @@ class BOM(WebsiteGenerator):
|
||||
continue
|
||||
|
||||
old_rate = d.rate
|
||||
if self.rm_cost_as_per != "Manual":
|
||||
if not self.bom_creator:
|
||||
d.rate = self.get_rm_rate(
|
||||
{
|
||||
"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"))
|
||||
price_list_data = get_price_list_rate(bom_args, item_doc)
|
||||
rate = price_list_data.price_list_rate
|
||||
elif bom_doc.rm_cost_as_per == "Manual":
|
||||
return
|
||||
|
||||
return flt(rate)
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
"fieldname": "rm_cost_as_per",
|
||||
"fieldtype": "Select",
|
||||
"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
|
||||
},
|
||||
{
|
||||
@@ -288,7 +288,7 @@
|
||||
"link_fieldname": "bom_creator"
|
||||
}
|
||||
],
|
||||
"modified": "2023-08-07 15:45:06.176313",
|
||||
"modified": "2024-04-02 16:30:59.779190",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM Creator",
|
||||
@@ -327,4 +327,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ class BOMCreator(Document):
|
||||
qty: DF.Float
|
||||
raw_material_cost: DF.Currency
|
||||
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
|
||||
status: DF.Literal["Draft", "Submitted", "In Progress", "Completed", "Failed", "Cancelled"]
|
||||
uom: DF.Link | None
|
||||
@@ -143,9 +143,6 @@ class BOMCreator(Document):
|
||||
self.submit()
|
||||
|
||||
def set_rate_for_items(self):
|
||||
if self.rm_cost_as_per == "Manual":
|
||||
return
|
||||
|
||||
amount = self.get_raw_material_cost()
|
||||
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})
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
reverse_tree = OrderedDict(reversed(list(production_item_wise_rm.items())))
|
||||
@@ -283,7 +283,6 @@ class BOMCreator(Document):
|
||||
"allow_alternative_item": 1,
|
||||
"bom_creator": self.name,
|
||||
"bom_creator_item": bom_creator_item,
|
||||
"rm_cost_as_per": "Manual",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -145,6 +145,7 @@ class Project(Document):
|
||||
is_group=task_details.is_group,
|
||||
color=task_details.color,
|
||||
template_task=task_details.name,
|
||||
priority=task_details.priority,
|
||||
)
|
||||
).insert()
|
||||
|
||||
|
||||
@@ -23,7 +23,11 @@ class TestProject(FrappeTestCase):
|
||||
task1 = task_exists("Test Template Task with No Parent and Dependency")
|
||||
if not task1:
|
||||
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(
|
||||
@@ -32,11 +36,12 @@ class TestProject(FrappeTestCase):
|
||||
project = get_project(project_name, template)
|
||||
tasks = frappe.get_all(
|
||||
"Task",
|
||||
["subject", "exp_end_date", "depends_on_tasks"],
|
||||
["subject", "exp_end_date", "depends_on_tasks", "priority"],
|
||||
dict(project=project.name),
|
||||
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(getdate(tasks[0].exp_end_date), calculate_end_date(project, 5, 3))
|
||||
self.assertEqual(len(tasks), 1)
|
||||
|
||||
@@ -122,6 +122,7 @@ def create_task(
|
||||
begin=0,
|
||||
duration=0,
|
||||
save=True,
|
||||
priority=None,
|
||||
):
|
||||
if not frappe.db.exists("Task", subject):
|
||||
task = frappe.new_doc("Task")
|
||||
@@ -139,6 +140,7 @@ def create_task(
|
||||
task.duration = duration
|
||||
task.is_group = is_group
|
||||
task.parent_task = parent_task
|
||||
task.priority = priority
|
||||
if save:
|
||||
task.save()
|
||||
else:
|
||||
|
||||
@@ -342,7 +342,6 @@ erpnext.buying = {
|
||||
add_serial_batch_bundle(doc, cdt, cdn) {
|
||||
let item = locals[cdt][cdn];
|
||||
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) => {
|
||||
@@ -352,30 +351,28 @@ erpnext.buying = {
|
||||
item.type_of_transaction = item.qty > 0 ? "Inward" : "Outward";
|
||||
item.is_rejected = false;
|
||||
|
||||
frappe.require(path, function() {
|
||||
new erpnext.SerialBatchPackageSelector(
|
||||
me.frm, item, (r) => {
|
||||
if (r) {
|
||||
let qty = Math.abs(r.total_qty);
|
||||
if (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);
|
||||
new erpnext.SerialBatchPackageSelector(
|
||||
me.frm, item, (r) => {
|
||||
if (r) {
|
||||
let qty = Math.abs(r.total_qty);
|
||||
if (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);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -383,40 +380,37 @@ erpnext.buying = {
|
||||
add_serial_batch_for_rejected_qty(doc, cdt, cdn) {
|
||||
let item = locals[cdt][cdn];
|
||||
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) => {
|
||||
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.type_of_transaction = item.rejected_qty > 0 ? "Inward" : "Outward";
|
||||
item.is_rejected = true;
|
||||
|
||||
frappe.require(path, function() {
|
||||
new erpnext.SerialBatchPackageSelector(
|
||||
me.frm, item, (r) => {
|
||||
if (r) {
|
||||
let qty = Math.abs(r.total_qty);
|
||||
if (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);
|
||||
new erpnext.SerialBatchPackageSelector(
|
||||
me.frm, item, (r) => {
|
||||
if (r) {
|
||||
let qty = Math.abs(r.total_qty);
|
||||
if (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);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -415,7 +415,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
let row = locals[cdt][cdn];
|
||||
if (row.barcode) {
|
||||
erpnext.stock.utils.set_item_details_using_barcode(this.frm, row, (r) => {
|
||||
debugger
|
||||
frappe.model.set_value(cdt, cdn, {
|
||||
"item_code": r.message.item_code,
|
||||
"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)) {
|
||||
item_row.type_of_transaction = frm.doc.is_return ? "Inward" : "Outward";
|
||||
} else {
|
||||
item_row.type_of_transaction = frm.doc.is_return ? "Outward" : "Inward";
|
||||
}
|
||||
if (["Sales Invoice", "Delivery Note"].includes(frm.doc.doctype)) {
|
||||
item_row.type_of_transaction = frm.doc.is_return ? "Inward" : "Outward";
|
||||
} else {
|
||||
item_row.type_of_transaction = frm.doc.is_return ? "Outward" : "Inward";
|
||||
}
|
||||
|
||||
new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => {
|
||||
if (r) {
|
||||
let update_values = {
|
||||
"serial_and_batch_bundle": r.name,
|
||||
"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);
|
||||
new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => {
|
||||
if (r) {
|
||||
let update_values = {
|
||||
"serial_and_batch_bundle": r.name,
|
||||
"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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import "./queries";
|
||||
import "./sms_manager";
|
||||
import "./utils/party";
|
||||
import "./controllers/stock_controller";
|
||||
import "./utils/serial_no_batch_selector";
|
||||
import "./payment/payments";
|
||||
import "./templates/visual_plant_floor_template.html";
|
||||
import "./plant_floor_visual/visual_plant";
|
||||
|
||||
@@ -430,25 +430,23 @@ $.extend(erpnext.utils, {
|
||||
item_row.has_batch_no = r.message.has_batch_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) => {
|
||||
if (r) {
|
||||
let update_values = {
|
||||
serial_and_batch_bundle: r.name,
|
||||
qty: Math.abs(r.total_qty),
|
||||
};
|
||||
new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => {
|
||||
if (r) {
|
||||
let update_values = {
|
||||
serial_and_batch_bundle: r.name,
|
||||
qty: Math.abs(r.total_qty),
|
||||
};
|
||||
|
||||
if (!warehouse_field) {
|
||||
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 (!warehouse_field) {
|
||||
warehouse_field = "warehouse";
|
||||
}
|
||||
});
|
||||
|
||||
if (r.warehouse) {
|
||||
update_values[warehouse_field] = r.warehouse;
|
||||
}
|
||||
|
||||
frappe.model.set_value(item_row.doctype, item_row.name, update_values);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@@ -350,7 +350,6 @@ erpnext.sales_common = {
|
||||
pick_serial_and_batch(doc, cdt, cdn) {
|
||||
let item = locals[cdt][cdn];
|
||||
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) => {
|
||||
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");
|
||||
}
|
||||
|
||||
frappe.require(path, function () {
|
||||
new erpnext.SerialBatchPackageSelector(me.frm, item, (r) => {
|
||||
if (r) {
|
||||
let qty = Math.abs(r.total_qty);
|
||||
if (doc.is_return) {
|
||||
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)
|
||||
),
|
||||
});
|
||||
new erpnext.SerialBatchPackageSelector(me.frm, item, (r) => {
|
||||
if (r) {
|
||||
let qty = Math.abs(r.total_qty);
|
||||
if (doc.is_return) {
|
||||
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)
|
||||
),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -394,19 +394,17 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
|
||||
bind_auto_serial_fetch_event() {
|
||||
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 item_row = this.item_row;
|
||||
item_row.type_of_transaction = "Outward";
|
||||
let frm = this.events.get_frm();
|
||||
let item_row = this.item_row;
|
||||
item_row.type_of_transaction = "Outward";
|
||||
|
||||
new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => {
|
||||
if (r) {
|
||||
frappe.model.set_value(item_row.doctype, item_row.name, {
|
||||
serial_and_batch_bundle: r.name,
|
||||
qty: Math.abs(r.total_qty),
|
||||
});
|
||||
}
|
||||
});
|
||||
new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => {
|
||||
if (r) {
|
||||
frappe.model.set_value(item_row.doctype, item_row.name, {
|
||||
serial_and_batch_bundle: r.name,
|
||||
qty: Math.abs(r.total_qty),
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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")):
|
||||
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):
|
||||
target_doc.inter_company_invoice_reference = source_doc.name
|
||||
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,
|
||||
)
|
||||
|
||||
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(
|
||||
doctype,
|
||||
source_name,
|
||||
@@ -1363,6 +1370,8 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
"Material_request_item": "material_request_item",
|
||||
},
|
||||
"field_no_map": ["warehouse"],
|
||||
"condition": lambda item: item.received_qty < item.qty + item.returned_qty,
|
||||
"postprocess": update_item,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
|
||||
@@ -32,7 +32,7 @@ test_ignore = ["BOM"]
|
||||
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:
|
||||
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:
|
||||
item.append("uoms", uom)
|
||||
|
||||
if barcode:
|
||||
item.append(
|
||||
"barcodes",
|
||||
{
|
||||
"barcode": barcode,
|
||||
},
|
||||
)
|
||||
|
||||
item.insert()
|
||||
|
||||
return item
|
||||
|
||||
@@ -355,19 +355,15 @@ frappe.ui.form.on("Pick List Item", {
|
||||
item.title = __("Select Serial and Batch");
|
||||
}
|
||||
|
||||
frappe.require(path, function () {
|
||||
new erpnext.SerialBatchPackageSelector(frm, item, (r) => {
|
||||
if (r) {
|
||||
let qty = Math.abs(r.total_qty);
|
||||
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)),
|
||||
});
|
||||
}
|
||||
});
|
||||
new erpnext.SerialBatchPackageSelector(frm, item, (r) => {
|
||||
if (r) {
|
||||
let qty = Math.abs(r.total_qty);
|
||||
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)),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"parent_warehouse",
|
||||
"consider_rejected_warehouses",
|
||||
"get_item_locations",
|
||||
"pick_manually",
|
||||
"section_break_6",
|
||||
"scan_barcode",
|
||||
"column_break_13",
|
||||
@@ -192,11 +193,18 @@
|
||||
"fieldname": "consider_rejected_warehouses",
|
||||
"fieldtype": "Check",
|
||||
"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,
|
||||
"links": [],
|
||||
"modified": "2024-02-02 16:17:44.877426",
|
||||
"modified": "2024-03-27 22:49:16.954637",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Pick List",
|
||||
|
||||
@@ -42,6 +42,7 @@ class PickList(Document):
|
||||
|
||||
amended_from: DF.Link | None
|
||||
company: DF.Link
|
||||
consider_rejected_warehouses: DF.Check
|
||||
customer: DF.Link | None
|
||||
customer_name: DF.Data | None
|
||||
for_qty: DF.Float
|
||||
@@ -50,6 +51,7 @@ class PickList(Document):
|
||||
material_request: DF.Link | None
|
||||
naming_series: DF.Literal["STO-PICK-.YYYY.-"]
|
||||
parent_warehouse: DF.Link | None
|
||||
pick_manually: DF.Check
|
||||
prompt_qty: DF.Check
|
||||
purpose: DF.Literal["Material Transfer for Manufacture", "Material Transfer", "Delivery"]
|
||||
scan_barcode: DF.Data | None
|
||||
@@ -71,7 +73,8 @@ class PickList(Document):
|
||||
|
||||
def before_save(self):
|
||||
self.update_status()
|
||||
self.set_item_locations()
|
||||
if not self.pick_manually:
|
||||
self.set_item_locations()
|
||||
|
||||
if self.get("locations"):
|
||||
self.validate_sales_order_percentage()
|
||||
|
||||
@@ -1335,18 +1335,16 @@ erpnext.stock.select_batch_and_serial_no = (frm, item) => {
|
||||
item.has_batch_no = r.message.has_batch_no;
|
||||
item.type_of_transaction = item.s_warehouse ? "Outward" : "Inward";
|
||||
|
||||
frappe.require(path, function () {
|
||||
new erpnext.SerialBatchPackageSelector(frm, item, (r) => {
|
||||
if (r) {
|
||||
frappe.model.set_value(item.doctype, item.name, {
|
||||
serial_and_batch_bundle: r.name,
|
||||
use_serial_batch_fields: 0,
|
||||
qty:
|
||||
Math.abs(r.total_qty) /
|
||||
flt(item.conversion_factor || 1, precision("conversion_factor", item)),
|
||||
});
|
||||
}
|
||||
});
|
||||
new erpnext.SerialBatchPackageSelector(frm, item, (r) => {
|
||||
if (r) {
|
||||
frappe.model.set_value(item.doctype, item.name, {
|
||||
serial_and_batch_bundle: r.name,
|
||||
use_serial_batch_fields: 0,
|
||||
qty:
|
||||
Math.abs(r.total_qty) /
|
||||
flt(item.conversion_factor || 1, precision("conversion_factor", item)),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -31,6 +31,7 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
|
||||
OpeningEntryAccountError,
|
||||
)
|
||||
from erpnext.stock.get_item_details import (
|
||||
get_barcode_data,
|
||||
get_bin_details,
|
||||
get_conversion_factor,
|
||||
get_default_cost_center,
|
||||
@@ -428,7 +429,14 @@ class StockEntry(StockController):
|
||||
for field in reset_fields:
|
||||
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:
|
||||
if not item.get(field):
|
||||
@@ -1609,6 +1617,10 @@ class StockEntry(StockController):
|
||||
if subcontract_items and len(subcontract_items) == 1:
|
||||
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
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -98,6 +98,12 @@ class TestStockEntry(FrappeTestCase):
|
||||
self._test_auto_material_request("_Test Item")
|
||||
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):
|
||||
fields = [{"field_name": "reorder_levels"}]
|
||||
set_item_variant_settings(fields)
|
||||
|
||||
@@ -81,6 +81,18 @@ frappe.ui.form.on("Stock Reconciliation", {
|
||||
if (frm.doc.company) {
|
||||
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) {
|
||||
@@ -155,6 +167,9 @@ frappe.ui.form.on("Stock Reconciliation", {
|
||||
|
||||
item.qty = item.qty || 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");
|
||||
},
|
||||
@@ -298,6 +313,10 @@ frappe.ui.form.on("Stock Reconciliation Item", {
|
||||
if (!item.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) {
|
||||
|
||||
@@ -49,6 +49,7 @@ frappe.ui.form.on("Warehouse", {
|
||||
frm.add_custom_button(__("Stock Balance"), function () {
|
||||
frappe.set_route("query-report", "Stock Balance", {
|
||||
warehouse: frm.doc.name,
|
||||
company: frm.doc.company,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -499,12 +499,21 @@ def update_barcode_value(out):
|
||||
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
|
||||
# example: {'LED-GRE': [Batch001, Batch002]}
|
||||
# where LED-GRE is item code, SN0001 is serial no and Pune is warehouse
|
||||
|
||||
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:
|
||||
barcodes = frappe.db.get_all(
|
||||
"Item Barcode", filters={"parent": item.item_code}, fields="barcode"
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
from collections.abc import Iterator
|
||||
from operator import itemgetter
|
||||
from typing import Dict, List, Tuple, Union
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
@@ -14,7 +14,7 @@ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
Filters = frappe._dict
|
||||
|
||||
|
||||
def execute(filters: Filters = None) -> Tuple:
|
||||
def execute(filters: Filters = None) -> tuple:
|
||||
to_date = filters["to_date"]
|
||||
columns = get_columns(filters)
|
||||
|
||||
@@ -26,14 +26,14 @@ def execute(filters: Filters = None) -> Tuple:
|
||||
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."
|
||||
_func = itemgetter(1)
|
||||
data = []
|
||||
|
||||
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):
|
||||
continue
|
||||
|
||||
@@ -74,12 +74,12 @@ def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> Li
|
||||
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
|
||||
for batch in fifo_queue:
|
||||
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]
|
||||
total_qty += batch[0]
|
||||
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
|
||||
|
||||
|
||||
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))
|
||||
|
||||
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
|
||||
|
||||
|
||||
def get_columns(filters: Filters) -> List[Dict]:
|
||||
def get_columns(filters: Filters) -> list[dict]:
|
||||
range_columns = []
|
||||
setup_ageing_columns(filters, range_columns)
|
||||
columns = [
|
||||
@@ -169,7 +168,7 @@ def get_columns(filters: Filters) -> List[Dict]:
|
||||
return columns
|
||||
|
||||
|
||||
def get_chart_data(data: List, filters: Filters) -> Dict:
|
||||
def get_chart_data(data: list, filters: Filters) -> dict:
|
||||
if not data:
|
||||
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 = [
|
||||
f"0 - {filters['range1']}",
|
||||
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)
|
||||
|
||||
|
||||
def add_column(
|
||||
range_columns: List, label: str, fieldname: str, fieldtype: str = "Float", width: int = 140
|
||||
):
|
||||
def add_column(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))
|
||||
|
||||
|
||||
class FIFOSlots:
|
||||
"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.transferred_item_details = {}
|
||||
self.serial_no_batch_purchase_details = {}
|
||||
self.filters = filters
|
||||
self.sle = sle
|
||||
|
||||
def generate(self) -> Dict:
|
||||
def generate(self) -> dict:
|
||||
"""
|
||||
Returns dict of the foll.g structure:
|
||||
Key = Item A / (Item A, Warehouse A)
|
||||
@@ -231,25 +228,45 @@ class FIFOSlots:
|
||||
consumed/updated and maintained via FIFO. **
|
||||
}
|
||||
"""
|
||||
if self.sle is None:
|
||||
self.sle = self.__get_stock_ledger_entries()
|
||||
|
||||
for d in self.sle:
|
||||
key, fifo_queue, transferred_item_key = self.__init_key_stores(d)
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
|
||||
get_serial_nos_from_bundle,
|
||||
)
|
||||
|
||||
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)
|
||||
stock_ledger_entries = self.sle
|
||||
|
||||
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:
|
||||
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)
|
||||
with frappe.db.unbuffered_cursor():
|
||||
if stock_ledger_entries is None:
|
||||
stock_ledger_entries = self.__get_stock_ledger_entries()
|
||||
|
||||
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"):
|
||||
# (Item 1, WH 1), (Item 1, WH 2) => (Item 1)
|
||||
@@ -257,7 +274,7 @@ class FIFOSlots:
|
||||
|
||||
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."
|
||||
|
||||
key = (row.name, row.warehouse)
|
||||
@@ -269,9 +286,7 @@ class FIFOSlots:
|
||||
|
||||
return key, fifo_queue, transferred_item_key
|
||||
|
||||
def __compute_incoming_stock(
|
||||
self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List
|
||||
):
|
||||
def __compute_incoming_stock(self, row: dict, fifo_queue: list, transfer_key: tuple, serial_nos: list):
|
||||
"Update FIFO Queue on inward stock."
|
||||
|
||||
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)
|
||||
fifo_queue.append([serial_no, row.posting_date])
|
||||
|
||||
def __compute_outgoing_stock(
|
||||
self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List
|
||||
):
|
||||
def __compute_outgoing_stock(self, row: dict, fifo_queue: list, transfer_key: tuple, serial_nos: list):
|
||||
"Update FIFO Queue on outward stock."
|
||||
if 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]])
|
||||
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."
|
||||
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]])
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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."
|
||||
item_aggregated_data = {}
|
||||
for key, row in wh_wise_data.items():
|
||||
@@ -370,7 +383,12 @@ class FIFOSlots:
|
||||
if not item_aggregated_data.get(item):
|
||||
item_aggregated_data.setdefault(
|
||||
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["details"].update(row["details"])
|
||||
@@ -381,7 +399,7 @@ class FIFOSlots:
|
||||
|
||||
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")
|
||||
item = self.__get_item_query() # used as derived table in sle query
|
||||
|
||||
@@ -403,6 +421,7 @@ class FIFOSlots:
|
||||
sle.serial_no,
|
||||
sle.batch_no,
|
||||
sle.qty_after_transaction,
|
||||
sle.serial_and_batch_bundle,
|
||||
sle.warehouse,
|
||||
)
|
||||
.where(
|
||||
@@ -418,7 +437,34 @@ class FIFOSlots:
|
||||
|
||||
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:
|
||||
item_table = frappe.qb.DocType("Item")
|
||||
|
||||
@@ -295,6 +295,8 @@ class StockBalanceReport(object):
|
||||
sle.stock_value,
|
||||
sle.batch_no,
|
||||
sle.serial_no,
|
||||
sle.serial_and_batch_bundle,
|
||||
sle.has_serial_no,
|
||||
item_table.item_group,
|
||||
item_table.stock_uom,
|
||||
item_table.item_name,
|
||||
|
||||
@@ -856,6 +856,11 @@ class SerialBatchCreation:
|
||||
if not doc.get("entries"):
|
||||
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()
|
||||
self.validate_qty(doc)
|
||||
|
||||
|
||||
@@ -335,12 +335,115 @@ frappe.ui.form.on("Subcontracting Receipt Item", {
|
||||
items_remove: (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", {
|
||||
consumed_qty(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) => {
|
||||
|
||||
@@ -47,9 +47,11 @@
|
||||
"schedule_date",
|
||||
"reference_name",
|
||||
"section_break_45",
|
||||
"add_serial_batch_bundle",
|
||||
"serial_and_batch_bundle",
|
||||
"use_serial_batch_fields",
|
||||
"col_break5",
|
||||
"add_serial_batch_for_rejected_qty",
|
||||
"rejected_serial_and_batch_bundle",
|
||||
"section_break_jshh",
|
||||
"serial_no",
|
||||
@@ -563,12 +565,24 @@
|
||||
{
|
||||
"fieldname": "column_break_henr",
|
||||
"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,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-07 11:43:38.954262",
|
||||
"modified": "2024-03-29 15:42:43.425544",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Receipt Item",
|
||||
@@ -579,4 +593,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"consumed_qty",
|
||||
"current_stock",
|
||||
"secbreak_3",
|
||||
"add_serial_batch_bundle",
|
||||
"serial_and_batch_bundle",
|
||||
"use_serial_batch_fields",
|
||||
"col_break4",
|
||||
@@ -224,12 +225,18 @@
|
||||
{
|
||||
"fieldname": "column_break_qibi",
|
||||
"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,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-04 16:32:17.534162",
|
||||
"modified": "2024-03-30 10:26:27.237371",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Receipt Supplied Item",
|
||||
@@ -240,4 +247,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"accept_payment": 0,
|
||||
"allow_comments": 1,
|
||||
"allow_delete": 1,
|
||||
"allow_edit": 1,
|
||||
"allow_incomplete": 0,
|
||||
"allow_multiple": 1,
|
||||
"allow_print": 0,
|
||||
"amount": 0.0,
|
||||
"amount_based_on_field": 0,
|
||||
"anonymous": 0,
|
||||
"apply_document_permissions": 0,
|
||||
"breadcrumbs": "[{\"label\":_(\"Issues\"), \"route\":\"issues\"}]",
|
||||
"condition_json": "[]",
|
||||
"creation": "2016-06-24 15:50:33.186483",
|
||||
"doc_type": "Issue",
|
||||
"docstatus": 0,
|
||||
@@ -16,20 +16,19 @@
|
||||
"idx": 0,
|
||||
"introduction_text": "",
|
||||
"is_standard": 1,
|
||||
"list_columns": [],
|
||||
"login_required": 1,
|
||||
"max_attachment_size": 0,
|
||||
"modified": "2020-05-19 13:01:10.729088",
|
||||
"modified": "2024-03-27 16:16:03.621730",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Support",
|
||||
"name": "issues",
|
||||
"owner": "Administrator",
|
||||
"published": 1,
|
||||
"route": "issues",
|
||||
"route_to_success_link": 0,
|
||||
"show_attachments": 0,
|
||||
"show_in_grid": 0,
|
||||
"show_list": 1,
|
||||
"show_sidebar": 1,
|
||||
"sidebar_items": [],
|
||||
"success_message": "",
|
||||
"success_url": "/issues",
|
||||
"title": "Issue",
|
||||
|
||||
Reference in New Issue
Block a user