mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-26 00:14:50 +00:00
Merge pull request #53916 from frappe/version-15-hotfix
This commit is contained in:
@@ -131,6 +131,7 @@ def get_default_company_bank_account(company, party_type, party):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_bank_account_details(bank_account):
|
def get_bank_account_details(bank_account):
|
||||||
|
frappe.has_permission("Bank Account", doc=bank_account, ptype="read", throw=True)
|
||||||
return frappe.get_cached_value(
|
return frappe.get_cached_value(
|
||||||
"Bank Account", bank_account, ["account", "bank", "bank_account_no"], as_dict=1
|
"Bank Account", bank_account, ["account", "bank", "bank_account_no"], as_dict=1
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _, scrub
|
from frappe import _, scrub
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import flt, nowdate
|
from frappe.utils import escape_html, flt, nowdate
|
||||||
from frappe.utils.background_jobs import enqueue, is_job_enqueued
|
from frappe.utils.background_jobs import enqueue, is_job_enqueued
|
||||||
|
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
@@ -84,6 +84,11 @@ class OpeningInvoiceCreationTool(Document):
|
|||||||
)
|
)
|
||||||
prepare_invoice_summary(doctype, invoices)
|
prepare_invoice_summary(doctype, invoices)
|
||||||
|
|
||||||
|
invoices_summary_companies = list(invoices_summary.keys())
|
||||||
|
|
||||||
|
for company in invoices_summary_companies:
|
||||||
|
invoices_summary[escape_html(company)] = invoices_summary.pop(company)
|
||||||
|
|
||||||
return invoices_summary, max_count
|
return invoices_summary, max_count
|
||||||
|
|
||||||
def validate_company(self):
|
def validate_company(self):
|
||||||
|
|||||||
@@ -612,12 +612,13 @@ class PurchaseInvoice(BuyingController):
|
|||||||
frappe.db.set_value(self.doctype, self.name, "against_expense_account", self.against_expense_account)
|
frappe.db.set_value(self.doctype, self.name, "against_expense_account", self.against_expense_account)
|
||||||
|
|
||||||
def po_required(self):
|
def po_required(self):
|
||||||
if frappe.db.get_value("Buying Settings", None, "po_required") == "Yes":
|
if (
|
||||||
if frappe.get_value(
|
frappe.db.get_single_value("Buying Settings", "po_required") == "Yes"
|
||||||
|
and not self.is_internal_transfer()
|
||||||
|
and not frappe.db.get_value(
|
||||||
"Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_order"
|
"Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_order"
|
||||||
):
|
)
|
||||||
return
|
):
|
||||||
|
|
||||||
for d in self.get("items"):
|
for d in self.get("items"):
|
||||||
if not d.purchase_order:
|
if not d.purchase_order:
|
||||||
msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code))
|
msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code))
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ class Deferred_Item:
|
|||||||
Generate report data for output
|
Generate report data for output
|
||||||
"""
|
"""
|
||||||
ret_data = frappe._dict({"name": self.item_name})
|
ret_data = frappe._dict({"name": self.item_name})
|
||||||
|
ret_data.service_start_date = self.service_start_date
|
||||||
|
ret_data.service_end_date = self.service_end_date
|
||||||
|
ret_data.amount = self.base_net_amount
|
||||||
for period in self.period_total:
|
for period in self.period_total:
|
||||||
ret_data[period.key] = period.total
|
ret_data[period.key] = period.total
|
||||||
ret_data.indent = 1
|
ret_data.indent = 1
|
||||||
@@ -205,6 +208,9 @@ class Deferred_Invoice:
|
|||||||
for item in self.uniq_items:
|
for item in self.uniq_items:
|
||||||
self.items.append(Deferred_Item(item, self, [x for x in items if x.item == item]))
|
self.items.append(Deferred_Item(item, self, [x for x in items if x.item == item]))
|
||||||
|
|
||||||
|
# roll-up amount from all deferred items
|
||||||
|
self.amount_total = sum(item.base_net_amount for item in self.items)
|
||||||
|
|
||||||
def calculate_invoice_revenue_expense_for_period(self):
|
def calculate_invoice_revenue_expense_for_period(self):
|
||||||
"""
|
"""
|
||||||
calculate deferred revenue/expense for all items in invoice
|
calculate deferred revenue/expense for all items in invoice
|
||||||
@@ -232,7 +238,7 @@ class Deferred_Invoice:
|
|||||||
generate report data for invoice, includes invoice total
|
generate report data for invoice, includes invoice total
|
||||||
"""
|
"""
|
||||||
ret_data = []
|
ret_data = []
|
||||||
inv_total = frappe._dict({"name": self.name})
|
inv_total = frappe._dict({"name": self.name, "amount": self.amount_total})
|
||||||
for x in self.period_total:
|
for x in self.period_total:
|
||||||
inv_total[x.key] = x.total
|
inv_total[x.key] = x.total
|
||||||
inv_total.indent = 0
|
inv_total.indent = 0
|
||||||
@@ -386,6 +392,24 @@ class Deferred_Revenue_and_Expense_Report:
|
|||||||
def get_columns(self):
|
def get_columns(self):
|
||||||
columns = []
|
columns = []
|
||||||
columns.append({"label": _("Name"), "fieldname": "name", "fieldtype": "Data", "read_only": 1})
|
columns.append({"label": _("Name"), "fieldname": "name", "fieldtype": "Data", "read_only": 1})
|
||||||
|
columns.append(
|
||||||
|
{
|
||||||
|
"label": _("Service Start Date"),
|
||||||
|
"fieldname": "service_start_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"read_only": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
columns.append(
|
||||||
|
{
|
||||||
|
"label": _("Service End Date"),
|
||||||
|
"fieldname": "service_end_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"read_only": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
columns.append({"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "read_only": 1})
|
||||||
|
|
||||||
for period in self.period_list:
|
for period in self.period_list:
|
||||||
columns.append(
|
columns.append(
|
||||||
{
|
{
|
||||||
@@ -415,6 +439,8 @@ class Deferred_Revenue_and_Expense_Report:
|
|||||||
elif self.filters.type == "Expense":
|
elif self.filters.type == "Expense":
|
||||||
total_row = frappe._dict({"name": "Total Deferred Expense"})
|
total_row = frappe._dict({"name": "Total Deferred Expense"})
|
||||||
|
|
||||||
|
total_row["amount"] = sum(inv.amount_total for inv in self.deferred_invoices)
|
||||||
|
|
||||||
for idx, period in enumerate(self.period_list, 0):
|
for idx, period in enumerate(self.period_list, 0):
|
||||||
total_row[period.key] = self.period_total[idx].total
|
total_row[period.key] = self.period_total[idx].total
|
||||||
ret.append(total_row)
|
ret.append(total_row)
|
||||||
|
|||||||
@@ -826,18 +826,18 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
|||||||
target.set_payment_schedule()
|
target.set_payment_schedule()
|
||||||
target.credit_to = get_party_account("Supplier", source.supplier, source.company)
|
target.credit_to = get_party_account("Supplier", source.supplier, source.company)
|
||||||
|
|
||||||
|
def get_billed_qty(po_item_name):
|
||||||
|
from frappe.query_builder.functions import Sum
|
||||||
|
|
||||||
|
table = frappe.qb.DocType("Purchase Invoice Item")
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(table)
|
||||||
|
.select(Sum(table.qty).as_("qty"))
|
||||||
|
.where((table.docstatus == 1) & (table.po_detail == po_item_name))
|
||||||
|
)
|
||||||
|
return query.run(pluck="qty")[0] or 0
|
||||||
|
|
||||||
def update_item(obj, target, source_parent):
|
def update_item(obj, target, source_parent):
|
||||||
def get_billed_qty(po_item_name):
|
|
||||||
from frappe.query_builder.functions import Sum
|
|
||||||
|
|
||||||
table = frappe.qb.DocType("Purchase Invoice Item")
|
|
||||||
query = (
|
|
||||||
frappe.qb.from_(table)
|
|
||||||
.select(Sum(table.qty).as_("qty"))
|
|
||||||
.where((table.docstatus == 1) & (table.po_detail == po_item_name))
|
|
||||||
)
|
|
||||||
return query.run(pluck="qty")[0] or 0
|
|
||||||
|
|
||||||
billed_qty = flt(get_billed_qty(obj.name))
|
billed_qty = flt(get_billed_qty(obj.name))
|
||||||
target.qty = flt(obj.qty) - billed_qty
|
target.qty = flt(obj.qty) - billed_qty
|
||||||
|
|
||||||
@@ -877,7 +877,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
|||||||
"wip_composite_asset": "wip_composite_asset",
|
"wip_composite_asset": "wip_composite_asset",
|
||||||
},
|
},
|
||||||
"postprocess": update_item,
|
"postprocess": update_item,
|
||||||
"condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount))
|
"condition": lambda doc: (
|
||||||
|
doc.base_amount == 0
|
||||||
|
or abs(doc.billed_amt) < abs(doc.amount)
|
||||||
|
or doc.qty > flt(get_billed_qty(doc.name))
|
||||||
|
)
|
||||||
and select_item(doc),
|
and select_item(doc),
|
||||||
},
|
},
|
||||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},
|
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},
|
||||||
|
|||||||
@@ -1346,6 +1346,35 @@ class TestPurchaseOrder(FrappeTestCase):
|
|||||||
self.assertEqual(pi_2.status, "Paid")
|
self.assertEqual(pi_2.status, "Paid")
|
||||||
self.assertEqual(po.status, "Completed")
|
self.assertEqual(po.status, "Completed")
|
||||||
|
|
||||||
|
@change_settings("Buying Settings", {"maintain_same_rate": 0})
|
||||||
|
def test_purchase_order_over_billing_missing_item(self):
|
||||||
|
item1 = make_item(
|
||||||
|
"_Test Item for Overbilling",
|
||||||
|
).name
|
||||||
|
|
||||||
|
item2 = make_item(
|
||||||
|
"_Test Item for Overbilling 2",
|
||||||
|
).name
|
||||||
|
|
||||||
|
po = create_purchase_order(qty=10, rate=1000, item_code=item1, do_not_save=1)
|
||||||
|
po.append("items", {"item_code": item2, "qty": 5, "rate": 20, "warehouse": "_Test Warehouse - _TC"})
|
||||||
|
po.taxes = []
|
||||||
|
po.insert()
|
||||||
|
po.submit()
|
||||||
|
|
||||||
|
pi1 = make_pi_from_po(po.name)
|
||||||
|
pi1.items[0].qty = 8
|
||||||
|
pi1.items[0].rate = 1250
|
||||||
|
pi1.remove(pi1.items[1])
|
||||||
|
pi1.insert()
|
||||||
|
pi1.submit()
|
||||||
|
|
||||||
|
self.assertEqual(pi1.grand_total, 10000.0)
|
||||||
|
self.assertTrue(len(pi1.items) == 1)
|
||||||
|
|
||||||
|
pi2 = make_pi_from_po(po.name)
|
||||||
|
self.assertEqual(len(pi2.items), 2)
|
||||||
|
|
||||||
|
|
||||||
def create_po_for_sc_testing():
|
def create_po_for_sc_testing():
|
||||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||||
|
|||||||
@@ -327,7 +327,7 @@ class BuyingController(SubcontractingController):
|
|||||||
last_item_idx = d.idx
|
last_item_idx = d.idx
|
||||||
|
|
||||||
total_valuation_amount = sum(
|
total_valuation_amount = sum(
|
||||||
flt(d.base_tax_amount_after_discount_amount)
|
flt(d.base_tax_amount_after_discount_amount) * (-1 if d.get("add_deduct_tax") == "Deduct" else 1)
|
||||||
for d in self.get("taxes")
|
for d in self.get("taxes")
|
||||||
if d.category in ["Valuation", "Valuation and Total"]
|
if d.category in ["Valuation", "Valuation and Total"]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -992,3 +992,26 @@ def get_item_uom_query(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
limit_page_length=page_len,
|
limit_page_length=page_len,
|
||||||
as_list=1,
|
as_list=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
@frappe.validate_and_sanitize_search_inputs
|
||||||
|
def get_warehouse_address(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
|
||||||
|
table = frappe.qb.DocType(doctype)
|
||||||
|
child_table = frappe.qb.DocType("Dynamic Link")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(table)
|
||||||
|
.inner_join(child_table)
|
||||||
|
.on((table.name == child_table.parent) & (child_table.parenttype == doctype))
|
||||||
|
.select(table.name)
|
||||||
|
.where(
|
||||||
|
(child_table.link_name == filters.get("warehouse"))
|
||||||
|
& (table.disabled == 0)
|
||||||
|
& (child_table.link_doctype == "Warehouse")
|
||||||
|
& (table.name.like(f"%{txt}%"))
|
||||||
|
)
|
||||||
|
.offset(start)
|
||||||
|
.limit(page_len)
|
||||||
|
)
|
||||||
|
return query.run(as_list=1)
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-12-07 10:44:22.587047",
|
"modified": "2026-03-25 19:27:19.162421",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "Contract Template",
|
"name": "Contract Template",
|
||||||
@@ -75,43 +75,34 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 1,
|
"export": 1,
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "Sales Manager",
|
"role": "Sales Manager",
|
||||||
"share": 1,
|
"share": 1
|
||||||
"write": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 1,
|
"export": 1,
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "Purchase Manager",
|
"role": "Purchase Manager",
|
||||||
"share": 1,
|
"share": 1
|
||||||
"write": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 1,
|
"export": 1,
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "HR Manager",
|
"role": "HR Manager",
|
||||||
"share": 1,
|
"share": 1
|
||||||
"write": 1
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,8 +204,22 @@ def send_mail(entry, email_campaign):
|
|||||||
|
|
||||||
# called from hooks on doc_event Email Unsubscribe
|
# called from hooks on doc_event Email Unsubscribe
|
||||||
def unsubscribe_recipient(unsubscribe, method):
|
def unsubscribe_recipient(unsubscribe, method):
|
||||||
if unsubscribe.reference_doctype == "Email Campaign":
|
if unsubscribe.reference_doctype != "Email Campaign":
|
||||||
frappe.db.set_value("Email Campaign", unsubscribe.reference_name, "status", "Unsubscribed")
|
return
|
||||||
|
|
||||||
|
email_campaign = frappe.get_doc("Email Campaign", unsubscribe.reference_name)
|
||||||
|
|
||||||
|
if email_campaign.email_campaign_for == "Email Group":
|
||||||
|
if unsubscribe.email:
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Email Group Member",
|
||||||
|
{"email_group": email_campaign.recipient, "email": unsubscribe.email},
|
||||||
|
"unsubscribed",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# For Lead or Contact
|
||||||
|
frappe.db.set_value("Email Campaign", email_campaign.name, "status", "Unsubscribed")
|
||||||
|
|
||||||
|
|
||||||
# called through hooks to update email campaign status daily
|
# called through hooks to update email campaign status daily
|
||||||
|
|||||||
@@ -767,12 +767,14 @@ class BOM(WebsiteGenerator):
|
|||||||
hour_rate / flt(self.conversion_rate) if self.conversion_rate and hour_rate else hour_rate
|
hour_rate / flt(self.conversion_rate) if self.conversion_rate and hour_rate else hour_rate
|
||||||
)
|
)
|
||||||
|
|
||||||
if row.hour_rate and row.time_in_mins:
|
if row.hour_rate:
|
||||||
row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate)
|
row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate)
|
||||||
row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0
|
|
||||||
row.base_operating_cost = flt(row.operating_cost) * flt(self.conversion_rate)
|
if row.time_in_mins:
|
||||||
row.cost_per_unit = row.operating_cost / (row.batch_size or 1.0)
|
row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0
|
||||||
row.base_cost_per_unit = row.base_operating_cost / (row.batch_size or 1.0)
|
row.base_operating_cost = flt(row.operating_cost) * flt(self.conversion_rate)
|
||||||
|
row.cost_per_unit = row.operating_cost / (row.batch_size or 1.0)
|
||||||
|
row.base_cost_per_unit = row.base_operating_cost / (row.batch_size or 1.0)
|
||||||
|
|
||||||
if update_hour_rate:
|
if update_hour_rate:
|
||||||
row.db_update()
|
row.db_update()
|
||||||
|
|||||||
@@ -31,6 +31,34 @@ frappe.ui.form.on("Job Card", {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frm.set_query("operation", "time_logs", () => {
|
||||||
|
let operations = (frm.doc.sub_operations || []).map((d) => d.sub_operation);
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
name: ["in", operations],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
frm.set_query("work_order", function () {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
status: ["not in", ["Cancelled", "Closed", "Stopped"]],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
frm.events.set_company_filters(frm, "target_warehouse");
|
||||||
|
frm.events.set_company_filters(frm, "source_warehouse");
|
||||||
|
frm.events.set_company_filters(frm, "wip_warehouse");
|
||||||
|
frm.set_query("source_warehouse", "items", () => {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
company: frm.doc.company,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
frm.set_indicator_formatter("sub_operation", function (doc) {
|
frm.set_indicator_formatter("sub_operation", function (doc) {
|
||||||
if (doc.status == "Pending") {
|
if (doc.status == "Pending") {
|
||||||
return "red";
|
return "red";
|
||||||
|
|||||||
@@ -1664,8 +1664,10 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
|
|||||||
)
|
)
|
||||||
|
|
||||||
sales_order = data.get("sales_order")
|
sales_order = data.get("sales_order")
|
||||||
|
qty_precision = frappe.get_precision("Material Request Plan Item", "quantity")
|
||||||
|
|
||||||
for item_code, details in item_details.items():
|
for item_code, details in item_details.items():
|
||||||
|
details.qty = flt(details.qty, qty_precision)
|
||||||
so_item_details.setdefault(sales_order, frappe._dict())
|
so_item_details.setdefault(sales_order, frappe._dict())
|
||||||
if item_code in so_item_details.get(sales_order, {}):
|
if item_code in so_item_details.get(sales_order, {}):
|
||||||
so_item_details[sales_order][item_code]["qty"] = so_item_details[sales_order][item_code].get(
|
so_item_details[sales_order][item_code]["qty"] = so_item_details[sales_order][item_code].get(
|
||||||
|
|||||||
@@ -383,7 +383,7 @@ class WorkOrder(Document):
|
|||||||
if self.docstatus == 0:
|
if self.docstatus == 0:
|
||||||
status = "Draft"
|
status = "Draft"
|
||||||
elif self.docstatus == 1:
|
elif self.docstatus == 1:
|
||||||
if status != "Stopped":
|
if status not in ["Closed", "Stopped"]:
|
||||||
status = "Not Started"
|
status = "Not Started"
|
||||||
if flt(self.material_transferred_for_manufacturing) > 0:
|
if flt(self.material_transferred_for_manufacturing) > 0:
|
||||||
status = "In Process"
|
status = "In Process"
|
||||||
|
|||||||
@@ -1018,11 +1018,11 @@ class TestQuotation(FrappeTestCase):
|
|||||||
def test_make_quotation_qar_to_inr(self):
|
def test_make_quotation_qar_to_inr(self):
|
||||||
quotation = make_quotation(
|
quotation = make_quotation(
|
||||||
currency="QAR",
|
currency="QAR",
|
||||||
transaction_date="2026-06-04",
|
transaction_date="2026-01-01",
|
||||||
)
|
)
|
||||||
|
|
||||||
cache = frappe.cache()
|
cache = frappe.cache()
|
||||||
key = "currency_exchange_rate_{}:{}:{}".format("2026-06-04", "QAR", "INR")
|
key = "currency_exchange_rate_{}:{}:{}".format("2026-01-01", "QAR", "INR")
|
||||||
value = cache.get(key)
|
value = cache.get(key)
|
||||||
expected_rate = flt(value) / 3.64
|
expected_rate = flt(value) / 3.64
|
||||||
|
|
||||||
|
|||||||
@@ -281,8 +281,13 @@ erpnext.stock.move_item = function (item, source, target, actual_qty, rate, stoc
|
|||||||
}
|
}
|
||||||
|
|
||||||
dialog.set_primary_action(__("Create Stock Entry"), function () {
|
dialog.set_primary_action(__("Create Stock Entry"), function () {
|
||||||
if (source && (dialog.get_value("qty") == 0 || dialog.get_value("qty") > actual_qty)) {
|
if (flt(dialog.get_value("qty")) <= 0) {
|
||||||
frappe.msgprint(__("Quantity must be greater than zero, and less or equal to {0}", [actual_qty]));
|
frappe.msgprint(__("Quantity must be greater than zero"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source && dialog.get_value("qty") > actual_qty) {
|
||||||
|
frappe.msgprint(__("Quantity must be less than or equal to {0}", [actual_qty]));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.model.db_query import DatabaseQuery
|
from frappe.model.db_query import DatabaseQuery
|
||||||
from frappe.utils import cint, flt
|
from frappe.utils import cint, escape_html, flt
|
||||||
|
|
||||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||||
get_sre_reserved_qty_for_items_and_warehouses as get_reserved_stock_details,
|
get_sre_reserved_qty_for_items_and_warehouses as get_reserved_stock_details,
|
||||||
@@ -70,8 +70,10 @@ def get_data(
|
|||||||
for item in items:
|
for item in items:
|
||||||
item.update(
|
item.update(
|
||||||
{
|
{
|
||||||
"item_name": frappe.get_cached_value("Item", item.item_code, "item_name"),
|
"item_code": escape_html(item.item_code),
|
||||||
"stock_uom": frappe.get_cached_value("Item", item.item_code, "stock_uom"),
|
"item_name": escape_html(frappe.get_cached_value("Item", item.item_code, "item_name")),
|
||||||
|
"stock_uom": escape_html(frappe.get_cached_value("Item", item.item_code, "stock_uom")),
|
||||||
|
"warehouse": escape_html(item.warehouse),
|
||||||
"disable_quick_entry": frappe.get_cached_value("Item", item.item_code, "has_batch_no")
|
"disable_quick_entry": frappe.get_cached_value("Item", item.item_code, "has_batch_no")
|
||||||
or frappe.get_cached_value("Item", item.item_code, "has_serial_no"),
|
or frappe.get_cached_value("Item", item.item_code, "has_serial_no"),
|
||||||
"projected_qty": flt(item.projected_qty, precision),
|
"projected_qty": flt(item.projected_qty, precision),
|
||||||
|
|||||||
@@ -50,15 +50,15 @@
|
|||||||
data-warehouse="{{ d.warehouse }}"
|
data-warehouse="{{ d.warehouse }}"
|
||||||
data-actual_qty="{{ d.actual_qty }}"
|
data-actual_qty="{{ d.actual_qty }}"
|
||||||
data-stock-uom="{{ d.stock_uom }}"
|
data-stock-uom="{{ d.stock_uom }}"
|
||||||
data-item="{{ escape(d.item_code) }}">{{ __("Move") }}</a>
|
data-item="{{ d.item_code }}">{{ __("Move") }}</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button style="margin-left: 7px;" class="btn btn-default btn-xs btn-add"
|
<button style="margin-left: 7px;" class="btn btn-default btn-xs btn-add"
|
||||||
data-disable_quick_entry="{{ d.disable_quick_entry }}"
|
data-disable_quick_entry="{{ d.disable_quick_entry }}"
|
||||||
data-warehouse="{{ d.warehouse }}"
|
data-warehouse="{{ d.warehouse }}"
|
||||||
data-actual_qty="{{ d.actual_qty }}"
|
data-actual_qty="{{ d.actual_qty }}"
|
||||||
data-stock-uom="{{ d.stock_uom }}"
|
data-stock-uom="{{ d.stock_uom }}"
|
||||||
data-item="{{ escape(d.item_code) }}"
|
data-item="{{ d.item_code }}"
|
||||||
data-rate="{{ d.valuation_rate }}">{{ __("Add") }}</a>
|
data-rate="{{ d.valuation_rate }}">{{ __("Add") }}</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.model.db_query import DatabaseQuery
|
from frappe.model.db_query import DatabaseQuery
|
||||||
from frappe.utils import flt, nowdate
|
from frappe.utils import escape_html, flt, nowdate
|
||||||
|
|
||||||
from erpnext.stock.utils import get_stock_balance
|
from erpnext.stock.utils import get_stock_balance
|
||||||
|
|
||||||
@@ -75,6 +75,9 @@ def get_warehouse_capacity_data(filters, start):
|
|||||||
balance_qty = get_stock_balance(entry.item_code, entry.warehouse, nowdate()) or 0
|
balance_qty = get_stock_balance(entry.item_code, entry.warehouse, nowdate()) or 0
|
||||||
entry.update(
|
entry.update(
|
||||||
{
|
{
|
||||||
|
"warehouse": escape_html(entry.warehouse),
|
||||||
|
"item_code": escape_html(entry.item_code),
|
||||||
|
"company": escape_html(entry.company),
|
||||||
"actual_qty": balance_qty,
|
"actual_qty": balance_qty,
|
||||||
"percent_occupied": flt((flt(balance_qty) / flt(entry.stock_capacity)) * 100, 0),
|
"percent_occupied": flt((flt(balance_qty) / flt(entry.stock_capacity)) * 100, 0),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from frappe.query_builder.custom import ConstantColumn
|
|||||||
from frappe.utils import cint, flt
|
from frappe.utils import cint, flt
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
|
from erpnext import is_perpetual_inventory_enabled
|
||||||
from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals
|
from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
|
||||||
@@ -160,6 +161,9 @@ class LandedCostVoucher(Document):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate_expense_accounts(self):
|
def validate_expense_accounts(self):
|
||||||
|
if not is_perpetual_inventory_enabled(self.company):
|
||||||
|
return
|
||||||
|
|
||||||
for t in self.taxes:
|
for t in self.taxes:
|
||||||
company = frappe.get_cached_value("Account", t.expense_account, "company")
|
company = frappe.get_cached_value("Account", t.expense_account, "company")
|
||||||
|
|
||||||
|
|||||||
@@ -175,6 +175,8 @@ class TestLandedCostVoucher(FrappeTestCase):
|
|||||||
self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0)
|
self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0)
|
||||||
|
|
||||||
def test_lcv_validates_company(self):
|
def test_lcv_validates_company(self):
|
||||||
|
from erpnext import is_perpetual_inventory_enabled
|
||||||
|
from erpnext.accounts.doctype.account.test_account import create_account
|
||||||
from erpnext.stock.doctype.landed_cost_voucher.landed_cost_voucher import (
|
from erpnext.stock.doctype.landed_cost_voucher.landed_cost_voucher import (
|
||||||
IncorrectCompanyValidationError,
|
IncorrectCompanyValidationError,
|
||||||
)
|
)
|
||||||
@@ -182,6 +184,20 @@ class TestLandedCostVoucher(FrappeTestCase):
|
|||||||
company_a = "_Test Company"
|
company_a = "_Test Company"
|
||||||
company_b = "_Test Company with perpetual inventory"
|
company_b = "_Test Company with perpetual inventory"
|
||||||
|
|
||||||
|
srbnb = create_account(
|
||||||
|
account_name="Stock Received But Not Billed",
|
||||||
|
account_type="Stock Received But Not Billed",
|
||||||
|
parent_account="Stock Liabilities - _TC",
|
||||||
|
company=company_a,
|
||||||
|
account_currency="INR",
|
||||||
|
)
|
||||||
|
|
||||||
|
epi = is_perpetual_inventory_enabled(company_a)
|
||||||
|
company_doc = frappe.get_doc("Company", company_a)
|
||||||
|
company_doc.enable_perpetual_inventory = 1
|
||||||
|
company_doc.stock_received_but_not_billed = srbnb
|
||||||
|
company_doc.save()
|
||||||
|
|
||||||
pr = make_purchase_receipt(
|
pr = make_purchase_receipt(
|
||||||
company=company_a,
|
company=company_a,
|
||||||
warehouse="Stores - _TC",
|
warehouse="Stores - _TC",
|
||||||
@@ -207,6 +223,9 @@ class TestLandedCostVoucher(FrappeTestCase):
|
|||||||
distribute_landed_cost_on_items(lcv)
|
distribute_landed_cost_on_items(lcv)
|
||||||
lcv.submit()
|
lcv.submit()
|
||||||
|
|
||||||
|
frappe.db.set_value("Company", company_a, "enable_perpetual_inventory", epi)
|
||||||
|
frappe.local.enable_perpetual_inventory = {}
|
||||||
|
|
||||||
def test_landed_cost_voucher_for_zero_purchase_rate(self):
|
def test_landed_cost_voucher_for_zero_purchase_rate(self):
|
||||||
"Test impact of LCV on future stock balances."
|
"Test impact of LCV on future stock balances."
|
||||||
from erpnext.stock.doctype.item.test_item import make_item
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
|||||||
@@ -505,8 +505,26 @@ class PickList(TransactionBase):
|
|||||||
self.item_location_map = frappe._dict()
|
self.item_location_map = frappe._dict()
|
||||||
|
|
||||||
from_warehouses = [self.parent_warehouse] if self.parent_warehouse else []
|
from_warehouses = [self.parent_warehouse] if self.parent_warehouse else []
|
||||||
if self.parent_warehouse:
|
|
||||||
from_warehouses.extend(get_descendants_of("Warehouse", self.parent_warehouse))
|
if self.work_order:
|
||||||
|
root_warehouse = frappe.db.get_value(
|
||||||
|
"Warehouse", {"company": self.company, "parent_warehouse": ["IS", "NOT SET"], "is_group": 1}
|
||||||
|
)
|
||||||
|
|
||||||
|
from_warehouses = [root_warehouse]
|
||||||
|
|
||||||
|
if from_warehouses:
|
||||||
|
from_warehouses.extend(get_descendants_of("Warehouse", from_warehouses[0]))
|
||||||
|
|
||||||
|
item_warehouse_dict = frappe._dict()
|
||||||
|
if self.work_order:
|
||||||
|
item_warehouse_list = frappe.get_all(
|
||||||
|
"Work Order Item",
|
||||||
|
filters={"parent": self.work_order},
|
||||||
|
fields=["item_code", "source_warehouse"],
|
||||||
|
)
|
||||||
|
if item_warehouse_list:
|
||||||
|
item_warehouse_dict = {item.item_code: item.source_warehouse for item in item_warehouse_list}
|
||||||
|
|
||||||
# Create replica before resetting, to handle empty table on update after submit.
|
# Create replica before resetting, to handle empty table on update after submit.
|
||||||
locations_replica = self.get("locations")
|
locations_replica = self.get("locations")
|
||||||
@@ -524,6 +542,13 @@ class PickList(TransactionBase):
|
|||||||
len_idx = len(self.get("locations")) or 0
|
len_idx = len(self.get("locations")) or 0
|
||||||
for item_doc in items:
|
for item_doc in items:
|
||||||
item_code = item_doc.item_code
|
item_code = item_doc.item_code
|
||||||
|
priority_warehouses = []
|
||||||
|
|
||||||
|
if self.work_order and item_warehouse_dict.get(item_code):
|
||||||
|
source_warehouse = item_warehouse_dict.get(item_code)
|
||||||
|
priority_warehouses = [source_warehouse]
|
||||||
|
priority_warehouses.extend(get_descendants_of("Warehouse", source_warehouse))
|
||||||
|
from_warehouses = list(dict.fromkeys(priority_warehouses + from_warehouses))
|
||||||
|
|
||||||
self.item_location_map.setdefault(
|
self.item_location_map.setdefault(
|
||||||
item_code,
|
item_code,
|
||||||
@@ -534,6 +559,7 @@ class PickList(TransactionBase):
|
|||||||
self.company,
|
self.company,
|
||||||
picked_item_details=picked_items_details.get(item_code),
|
picked_item_details=picked_items_details.get(item_code),
|
||||||
consider_rejected_warehouses=self.consider_rejected_warehouses,
|
consider_rejected_warehouses=self.consider_rejected_warehouses,
|
||||||
|
priority_warehouses=priority_warehouses,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -959,6 +985,7 @@ def get_available_item_locations(
|
|||||||
ignore_validation=False,
|
ignore_validation=False,
|
||||||
picked_item_details=None,
|
picked_item_details=None,
|
||||||
consider_rejected_warehouses=False,
|
consider_rejected_warehouses=False,
|
||||||
|
priority_warehouses=None,
|
||||||
):
|
):
|
||||||
locations = []
|
locations = []
|
||||||
|
|
||||||
@@ -999,7 +1026,7 @@ def get_available_item_locations(
|
|||||||
locations = filter_locations_by_picked_materials(locations, picked_item_details)
|
locations = filter_locations_by_picked_materials(locations, picked_item_details)
|
||||||
|
|
||||||
if locations:
|
if locations:
|
||||||
locations = get_locations_based_on_required_qty(locations, required_qty)
|
locations = get_locations_based_on_required_qty(locations, required_qty, priority_warehouses)
|
||||||
|
|
||||||
if not ignore_validation:
|
if not ignore_validation:
|
||||||
validate_picked_materials(item_code, required_qty, locations, picked_item_details)
|
validate_picked_materials(item_code, required_qty, locations, picked_item_details)
|
||||||
@@ -1007,9 +1034,14 @@ def get_available_item_locations(
|
|||||||
return locations
|
return locations
|
||||||
|
|
||||||
|
|
||||||
def get_locations_based_on_required_qty(locations, required_qty):
|
def get_locations_based_on_required_qty(locations, required_qty, priority_warehouses):
|
||||||
filtered_locations = []
|
filtered_locations = []
|
||||||
|
|
||||||
|
if priority_warehouses:
|
||||||
|
priority_locations = [loc for loc in locations if loc.warehouse in priority_warehouses]
|
||||||
|
fallback_locations = [loc for loc in locations if loc.warehouse not in priority_warehouses]
|
||||||
|
locations = priority_locations + fallback_locations
|
||||||
|
|
||||||
for location in locations:
|
for location in locations:
|
||||||
if location.qty >= required_qty:
|
if location.qty >= required_qty:
|
||||||
location.qty = required_qty
|
location.qty = required_qty
|
||||||
|
|||||||
@@ -1041,6 +1041,53 @@ class TestPickList(FrappeTestCase):
|
|||||||
pl = create_pick_list(so.name)
|
pl = create_pick_list(so.name)
|
||||||
self.assertFalse(pl.locations)
|
self.assertFalse(pl.locations)
|
||||||
|
|
||||||
|
def test_pick_list_warehouse_for_work_order(self):
|
||||||
|
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||||
|
from erpnext.manufacturing.doctype.work_order.work_order import create_pick_list, make_work_order
|
||||||
|
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||||
|
|
||||||
|
# Create Warehouses for Work Order
|
||||||
|
source_warehouse = create_warehouse("_Test WO Warehouse")
|
||||||
|
wip_warehouse = create_warehouse("_Test WIP Warehouse", company="_Test Company")
|
||||||
|
fg_warehouse = create_warehouse("_Test Finished Goods Warehouse", company="_Test Company")
|
||||||
|
|
||||||
|
# Create Finished Good Item
|
||||||
|
fg_item = make_item("Test Work Order Finished Good Item", properties={"is_stock_item": 1}).name
|
||||||
|
|
||||||
|
# Create Raw Material Item
|
||||||
|
rm_item = make_item("Test Work Order Raw Material Item", properties={"is_stock_item": 1}).name
|
||||||
|
|
||||||
|
# Create BOM
|
||||||
|
bom = make_bom(item=fg_item, rate=100, raw_materials=[rm_item])
|
||||||
|
|
||||||
|
# Create Inward entry for Raw Material
|
||||||
|
make_stock_entry(item=rm_item, to_warehouse=wip_warehouse, qty=10)
|
||||||
|
make_stock_entry(item=rm_item, to_warehouse=source_warehouse, qty=10)
|
||||||
|
|
||||||
|
# Create Work Order
|
||||||
|
wo = make_work_order(item=fg_item, qty=5, bom_no=bom.name, company="_Test Company")
|
||||||
|
wo.required_items[0].source_warehouse = source_warehouse
|
||||||
|
wo.fg_warehouse = fg_warehouse
|
||||||
|
wo.skip_transfer = True
|
||||||
|
wo.submit()
|
||||||
|
|
||||||
|
# Create Pick List
|
||||||
|
pl = create_pick_list(wo.name, for_qty=wo.qty)
|
||||||
|
|
||||||
|
# System prioritises the Source Warehouse
|
||||||
|
self.assertEqual(pl.locations[0].warehouse, source_warehouse)
|
||||||
|
self.assertEqual(pl.locations[0].item_code, rm_item)
|
||||||
|
self.assertEqual(pl.locations[0].qty, 5)
|
||||||
|
|
||||||
|
# Create Outward Entry from Source Warehouse
|
||||||
|
make_stock_entry(item=rm_item, from_warehouse=source_warehouse, qty=10)
|
||||||
|
pl.set_item_locations()
|
||||||
|
|
||||||
|
# System should pick other available warehouses
|
||||||
|
self.assertEqual(pl.locations[0].warehouse, wip_warehouse)
|
||||||
|
self.assertEqual(pl.locations[0].item_code, rm_item)
|
||||||
|
self.assertEqual(pl.locations[0].qty, 5)
|
||||||
|
|
||||||
def test_pick_list_validation_for_serial_no(self):
|
def test_pick_list_validation_for_serial_no(self):
|
||||||
warehouse = "_Test Warehouse - _TC"
|
warehouse = "_Test Warehouse - _TC"
|
||||||
item = make_item(
|
item = make_item(
|
||||||
|
|||||||
@@ -1217,6 +1217,65 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
|
|
||||||
pr.cancel()
|
pr.cancel()
|
||||||
|
|
||||||
|
def test_item_valuation_with_deduct_valuation_and_total_tax(self):
|
||||||
|
pr = make_purchase_receipt(
|
||||||
|
company="_Test Company with perpetual inventory",
|
||||||
|
warehouse="Stores - TCP1",
|
||||||
|
supplier_warehouse="Work In Progress - TCP1",
|
||||||
|
qty=5,
|
||||||
|
rate=100,
|
||||||
|
do_not_save=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
pr.append(
|
||||||
|
"taxes",
|
||||||
|
{
|
||||||
|
"charge_type": "Actual",
|
||||||
|
"add_deduct_tax": "Deduct",
|
||||||
|
"account_head": "_Test Account Shipping Charges - TCP1",
|
||||||
|
"category": "Valuation and Total",
|
||||||
|
"cost_center": "Main - TCP1",
|
||||||
|
"description": "Valuation Discount",
|
||||||
|
"tax_amount": 20,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
pr.insert()
|
||||||
|
|
||||||
|
self.assertAlmostEqual(pr.items[0].item_tax_amount, -20.0, places=2)
|
||||||
|
self.assertAlmostEqual(pr.items[0].valuation_rate, 96.0, places=2)
|
||||||
|
|
||||||
|
pr.delete()
|
||||||
|
|
||||||
|
pr = make_purchase_receipt(
|
||||||
|
company="_Test Company with perpetual inventory",
|
||||||
|
warehouse="Stores - TCP1",
|
||||||
|
supplier_warehouse="Work In Progress - TCP1",
|
||||||
|
qty=5,
|
||||||
|
rate=100,
|
||||||
|
do_not_save=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
pr.append(
|
||||||
|
"taxes",
|
||||||
|
{
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"add_deduct_tax": "Deduct",
|
||||||
|
"account_head": "_Test Account Shipping Charges - TCP1",
|
||||||
|
"category": "Valuation and Total",
|
||||||
|
"cost_center": "Main - TCP1",
|
||||||
|
"description": "Valuation Discount",
|
||||||
|
"rate": 10,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
pr.insert()
|
||||||
|
|
||||||
|
self.assertAlmostEqual(pr.items[0].item_tax_amount, -50.0, places=2)
|
||||||
|
self.assertAlmostEqual(pr.items[0].valuation_rate, 90.0, places=2)
|
||||||
|
|
||||||
|
pr.delete()
|
||||||
|
|
||||||
def test_po_to_pi_and_po_to_pr_worflow_full(self):
|
def test_po_to_pi_and_po_to_pr_worflow_full(self):
|
||||||
"""Test following behaviour:
|
"""Test following behaviour:
|
||||||
- Create PO
|
- Create PO
|
||||||
|
|||||||
@@ -14,19 +14,19 @@
|
|||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldname": "length",
|
"fieldname": "length",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Length (cm)"
|
"label": "Length (cm)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "width",
|
"fieldname": "width",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Width (cm)"
|
"label": "Width (cm)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "height",
|
"fieldname": "height",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Height (cm)"
|
"label": "Height (cm)"
|
||||||
},
|
},
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-06 16:48:57.355757",
|
"modified": "2026-03-29 00:00:00.000000",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Shipment Parcel",
|
"name": "Shipment Parcel",
|
||||||
@@ -60,4 +60,4 @@
|
|||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,21 +15,21 @@
|
|||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldname": "length",
|
"fieldname": "length",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Length (cm)",
|
"label": "Length (cm)",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "width",
|
"fieldname": "width",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Width (cm)",
|
"label": "Width (cm)",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "height",
|
"fieldname": "height",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Height (cm)",
|
"label": "Height (cm)",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-09-28 12:51:00.320421",
|
"modified": "2026-03-29 00:00:00.000000",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Shipment Parcel Template",
|
"name": "Shipment Parcel Template",
|
||||||
@@ -75,4 +75,4 @@
|
|||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,18 +38,18 @@ frappe.ui.form.on("Stock Entry", {
|
|||||||
|
|
||||||
frm.set_query("source_warehouse_address", function () {
|
frm.set_query("source_warehouse_address", function () {
|
||||||
return {
|
return {
|
||||||
|
query: "erpnext.controllers.queries.get_warehouse_address",
|
||||||
filters: {
|
filters: {
|
||||||
link_doctype: "Warehouse",
|
warehouse: frm.doc.from_warehouse,
|
||||||
link_name: frm.doc.from_warehouse,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
frm.set_query("target_warehouse_address", function () {
|
frm.set_query("target_warehouse_address", function () {
|
||||||
return {
|
return {
|
||||||
|
query: "erpnext.controllers.queries.get_warehouse_address",
|
||||||
filters: {
|
filters: {
|
||||||
link_doctype: "Warehouse",
|
warehouse: frm.doc.to_warehouse,
|
||||||
link_name: frm.doc.to_warehouse,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from operator import itemgetter
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.query_builder.functions import Count
|
||||||
from frappe.utils import cint, date_diff, flt, get_datetime
|
from frappe.utils import cint, date_diff, flt, get_datetime
|
||||||
|
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
@@ -238,9 +239,9 @@ class FIFOSlots:
|
|||||||
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)
|
||||||
Key: {
|
Key: {
|
||||||
'details' -> Dict: ** item details **,
|
'details' -> Dict: ** item details **,
|
||||||
'fifo_queue' -> List: ** list of lists containing entries/slots for existing stock,
|
'fifo_queue' -> List: ** list of lists containing entries/slots for existing stock,
|
||||||
consumed/updated and maintained via FIFO. **
|
consumed/updated and maintained via FIFO. **
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
from erpnext.stock.serial_batch_bundle import get_serial_nos_from_bundle
|
from erpnext.stock.serial_batch_bundle import get_serial_nos_from_bundle
|
||||||
@@ -251,16 +252,33 @@ class FIFOSlots:
|
|||||||
if stock_ledger_entries is None:
|
if stock_ledger_entries is None:
|
||||||
bundle_wise_serial_nos = self.__get_bundle_wise_serial_nos()
|
bundle_wise_serial_nos = self.__get_bundle_wise_serial_nos()
|
||||||
|
|
||||||
|
# prepare single sle voucher detail lookup
|
||||||
|
self.prepare_stock_reco_voucher_wise_count()
|
||||||
|
|
||||||
with frappe.db.unbuffered_cursor():
|
with frappe.db.unbuffered_cursor():
|
||||||
if stock_ledger_entries is None:
|
if stock_ledger_entries is None:
|
||||||
stock_ledger_entries = self.__get_stock_ledger_entries()
|
stock_ledger_entries = self.__get_stock_ledger_entries()
|
||||||
|
|
||||||
for d in stock_ledger_entries:
|
for d in stock_ledger_entries:
|
||||||
key, fifo_queue, transferred_item_key = self.__init_key_stores(d)
|
key, fifo_queue, transferred_item_key = self.__init_key_stores(d)
|
||||||
|
prev_balance_qty = self.item_details[key].get("qty_after_transaction", 0)
|
||||||
|
|
||||||
if d.voucher_type == "Stock Reconciliation":
|
if d.voucher_type == "Stock Reconciliation" and (
|
||||||
|
not d.batch_no or d.serial_no or d.serial_and_batch_bundle
|
||||||
|
):
|
||||||
|
if d.voucher_detail_no in self.stock_reco_voucher_wise_count:
|
||||||
|
# for legacy recon with single sle has qty_after_transaction and stock_value_difference without outward entry
|
||||||
|
# for exisitng handle emptying the existing queue and details.
|
||||||
|
d.stock_value_difference = flt(d.qty_after_transaction * d.valuation_rate)
|
||||||
|
d.actual_qty = d.qty_after_transaction
|
||||||
|
self.item_details[key]["qty_after_transaction"] = 0
|
||||||
|
self.item_details[key]["total_qty"] = 0
|
||||||
|
fifo_queue.clear()
|
||||||
|
else:
|
||||||
|
d.actual_qty = flt(d.qty_after_transaction) - flt(prev_balance_qty)
|
||||||
|
|
||||||
|
elif d.voucher_type == "Stock Reconciliation":
|
||||||
# get difference in qty shift as actual qty
|
# 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)
|
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 []
|
serial_nos = get_serial_nos(d.serial_no) if d.serial_no else []
|
||||||
@@ -278,6 +296,14 @@ class FIFOSlots:
|
|||||||
|
|
||||||
self.__update_balances(d, key)
|
self.__update_balances(d, key)
|
||||||
|
|
||||||
|
# handle serial nos misconsumption
|
||||||
|
if d.has_serial_no:
|
||||||
|
qty_after = cint(self.item_details[key]["qty_after_transaction"])
|
||||||
|
if qty_after <= 0:
|
||||||
|
fifo_queue.clear()
|
||||||
|
elif len(fifo_queue) > qty_after:
|
||||||
|
fifo_queue[:] = fifo_queue[:qty_after]
|
||||||
|
|
||||||
# Note that stock_ledger_entries is an iterator, you can not reuse it like a list
|
# Note that stock_ledger_entries is an iterator, you can not reuse it like a list
|
||||||
del stock_ledger_entries
|
del stock_ledger_entries
|
||||||
|
|
||||||
@@ -404,7 +430,6 @@ class FIFOSlots:
|
|||||||
|
|
||||||
def __update_balances(self, row: dict, key: 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]:
|
||||||
self.item_details[key]["total_qty"] = row.actual_qty
|
self.item_details[key]["total_qty"] = row.actual_qty
|
||||||
else:
|
else:
|
||||||
@@ -460,6 +485,7 @@ class FIFOSlots:
|
|||||||
sle.posting_date,
|
sle.posting_date,
|
||||||
sle.voucher_type,
|
sle.voucher_type,
|
||||||
sle.voucher_no,
|
sle.voucher_no,
|
||||||
|
sle.voucher_detail_no,
|
||||||
sle.serial_no,
|
sle.serial_no,
|
||||||
sle.batch_no,
|
sle.batch_no,
|
||||||
sle.qty_after_transaction,
|
sle.qty_after_transaction,
|
||||||
@@ -555,3 +581,36 @@ class FIFOSlots:
|
|||||||
warehouse_results = [x[0] for x in warehouse_results]
|
warehouse_results = [x[0] for x in warehouse_results]
|
||||||
|
|
||||||
return sle_query.where(sle.warehouse.isin(warehouse_results))
|
return sle_query.where(sle.warehouse.isin(warehouse_results))
|
||||||
|
|
||||||
|
def prepare_stock_reco_voucher_wise_count(self):
|
||||||
|
self.stock_reco_voucher_wise_count = frappe._dict()
|
||||||
|
|
||||||
|
doctype = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
|
item = frappe.qb.DocType("Item")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(doctype)
|
||||||
|
.inner_join(item)
|
||||||
|
.on(doctype.item_code == item.name)
|
||||||
|
.select(doctype.voucher_detail_no, Count(doctype.name).as_("count"))
|
||||||
|
.where(
|
||||||
|
(doctype.voucher_type == "Stock Reconciliation")
|
||||||
|
& (doctype.docstatus < 2)
|
||||||
|
& (doctype.is_cancelled == 0)
|
||||||
|
)
|
||||||
|
.groupby(doctype.voucher_detail_no)
|
||||||
|
)
|
||||||
|
|
||||||
|
data = query.run(as_dict=True)
|
||||||
|
if not data:
|
||||||
|
return
|
||||||
|
|
||||||
|
for row in data:
|
||||||
|
if row.count != 1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
sr_item = frappe.db.get_value(
|
||||||
|
"Stock Reconciliation Item", row.voucher_detail_no, ["current_qty", "qty"], as_dict=True
|
||||||
|
)
|
||||||
|
if sr_item.qty and sr_item.current_qty:
|
||||||
|
self.stock_reco_voucher_wise_count[row.voucher_detail_no] = sr_item.current_qty
|
||||||
|
|||||||
@@ -140,7 +140,7 @@
|
|||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
{% for attachment in attachments %}
|
{% for attachment in attachments %}
|
||||||
<p class="small">
|
<p class="small">
|
||||||
<a href="{{ attachment.file_url }}" target="blank"> {{ attachment.file_name }} </a>
|
<a href="{{ attachment.file_url|e }}" target="blank"> {{ attachment.file_name|e }} </a>
|
||||||
</p>
|
</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -82,11 +82,11 @@
|
|||||||
<div class="project-attachments">
|
<div class="project-attachments">
|
||||||
{% for attachment in doc.attachments %}
|
{% for attachment in doc.attachments %}
|
||||||
<div class="attachment">
|
<div class="attachment">
|
||||||
<a class="no-decoration attachment-link" href="{{ attachment.file_url }}" target="blank">
|
<a class="no-decoration attachment-link" href="{{ attachment.file_url|e }}" target="blank">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-9">
|
<div class="col-xs-9">
|
||||||
<span class="indicator red file-name">
|
<span class="indicator red file-name">
|
||||||
{{ attachment.file_name }}</span>
|
{{ attachment.file_name|e }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-3">
|
<div class="col-xs-3">
|
||||||
<span class="pull-right file-size">{{ attachment.file_size }}</span>
|
<span class="pull-right file-size">{{ attachment.file_size }}</span>
|
||||||
@@ -101,8 +101,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
{ % include "frappe/public/js/frappe/provide.js" % }
|
{% include "frappe/public/js/frappe/provide.js" %}
|
||||||
{ % include "frappe/public/js/frappe/form/formatters.js" % }
|
{% include "frappe/public/js/frappe/form/formatters.js" %}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user