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

chore: release v15
This commit is contained in:
ruthra kumar
2024-08-28 10:33:09 +05:30
committed by GitHub
45 changed files with 1105 additions and 88 deletions

View File

@@ -6,7 +6,7 @@ import frappe
from frappe import _, msgprint
from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, fmt_money, getdate
from frappe.utils import flt, fmt_money, get_link_to_form, getdate
from pypika import Order
import erpnext
@@ -96,8 +96,11 @@ class BankClearance(Document):
if d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date):
frappe.throw(
_("Row #{0}: Clearance date {1} cannot be before Cheque Date {2}").format(
d.idx, d.clearance_date, d.cheque_date
_("Row #{0}: For {1} Clearance date {2} cannot be before Cheque Date {3}").format(
d.idx,
get_link_to_form(d.payment_document, d.payment_entry),
d.clearance_date,
d.cheque_date,
)
)

View File

@@ -35,6 +35,11 @@ frappe.ui.form.on("Payment Entry", {
var account_types = ["Pay", "Internal Transfer"].includes(frm.doc.payment_type)
? ["Bank", "Cash"]
: [frappe.boot.party_account_types[frm.doc.party_type]];
if (frm.doc.party_type == "Shareholder") {
account_types.push("Equity");
}
return {
filters: {
account_type: ["in", account_types],
@@ -90,6 +95,9 @@ frappe.ui.form.on("Payment Entry", {
var account_types = ["Receive", "Internal Transfer"].includes(frm.doc.payment_type)
? ["Bank", "Cash"]
: [frappe.boot.party_account_types[frm.doc.party_type]];
if (frm.doc.party_type == "Shareholder") {
account_types.push("Equity");
}
return {
filters: {
account_type: ["in", account_types],
@@ -412,6 +420,12 @@ frappe.ui.form.on("Payment Entry", {
return {
query: "erpnext.controllers.queries.employee_query",
};
} else if (frm.doc.party_type == "Shareholder") {
return {
filters: {
company: frm.doc.company,
},
};
}
});

View File

@@ -1740,7 +1740,7 @@ def get_outstanding_reference_documents(args, validate=False):
d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
# Get negative outstanding sales /purchase invoices
if args.get("party_type") != "Employee" and not args.get("voucher_no"):
if args.get("party_type") != "Employee":
negative_outstanding_invoices = get_negative_outstanding_invoices(
args.get("party_type"),
args.get("party"),

View File

@@ -15,7 +15,7 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import (
)
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
from erpnext.accounts.party import get_party_account, get_party_bank_account
from erpnext.accounts.utils import get_account_currency
from erpnext.accounts.utils import get_account_currency, get_currency_precision
from erpnext.utilities import payment_app_import_guard
@@ -552,7 +552,7 @@ def get_amount(ref_doc, payment_account=None):
grand_total = ref_doc.outstanding_amount
if grand_total > 0:
return grand_total
return flt(grand_total, get_currency_precision())
else:
frappe.throw(_("Payment Entry is already created"))

View File

@@ -486,7 +486,7 @@ def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules, row_item):
continue
stock_qty = row.get("qty") * (row.get("conversion_factor") or 1.0)
amount = stock_qty * (row.get("price_list_rate") or row.get("rate"))
amount = stock_qty * (flt(row.get("price_list_rate")) or flt(row.get("rate")))
pricing_rules = filter_pricing_rules_for_qty_amount(stock_qty, amount, pricing_rules, row)
if pricing_rules and pricing_rules[0]:

View File

@@ -2236,6 +2236,62 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
self.assertEqual(pi_expected_values[i][1], gle.debit)
self.assertEqual(pi_expected_values[i][2], gle.credit)
def test_adjust_incoming_rate_from_pi_with_multi_currency(self):
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
make_landed_cost_voucher,
)
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
# Increase the cost of the item
pr = make_purchase_receipt(
qty=10, rate=1, currency="USD", do_not_save=1, supplier="_Test Supplier USD"
)
pr.conversion_rate = 6300
pr.plc_conversion_rate = 1
pr.save()
pr.submit()
self.assertEqual(pr.conversion_rate, 6300)
self.assertEqual(pr.plc_conversion_rate, 1)
self.assertEqual(pr.base_grand_total, 6300 * 10)
stock_value_difference = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
"stock_value_difference",
)
self.assertEqual(stock_value_difference, 6300 * 10)
make_landed_cost_voucher(
company=pr.company,
receipt_document_type="Purchase Receipt",
receipt_document=pr.name,
charges=3000,
distribute_charges_based_on="Qty",
)
pi = create_purchase_invoice_from_receipt(pr.name)
for row in pi.items:
row.rate = 1.1
pi.save()
pi.submit()
stock_value_difference = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
"stock_value_difference",
)
self.assertEqual(stock_value_difference, 7230 * 10)
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(

View File

@@ -737,10 +737,7 @@ class Subscription(Document):
elif self.generate_invoice_at == "Days before the current subscription period":
processing_date = add_days(self.current_invoice_start, -self.number_of_days)
process_subscription = frappe.new_doc("Process Subscription")
process_subscription.posting_date = processing_date
process_subscription.subscription = self.name
process_subscription.save().submit()
self.process(posting_date=processing_date)
def is_prorate() -> int:

View File

@@ -46,5 +46,11 @@ frappe.query_reports["Asset Depreciations and Balances"] = {
options: "Asset",
depends_on: "eval: doc.group_by == 'Asset'",
},
{
fieldname: "finance_book",
label: __("Finance Book"),
fieldtype: "Link",
options: "Finance Book",
},
],
};

View File

@@ -69,6 +69,9 @@ def get_asset_categories_for_grouped_by_category(filters):
condition = ""
if filters.get("asset_category"):
condition += " and asset_category = %(asset_category)s"
if filters.get("finance_book"):
condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)"
# nosemgrep
return frappe.db.sql(
f"""
@@ -119,6 +122,7 @@ def get_asset_categories_for_grouped_by_category(filters):
"from_date": filters.from_date,
"company": filters.company,
"asset_category": filters.get("asset_category"),
"finance_book": filters.get("finance_book"),
},
as_dict=1,
)
@@ -128,6 +132,10 @@ def get_asset_details_for_grouped_by_category(filters):
condition = ""
if filters.get("asset"):
condition += " and name = %(asset)s"
if filters.get("finance_book"):
condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = `tabAsset`.name and ads.finance_book = %(finance_book)s)"
# nosemgrep
return frappe.db.sql(
f"""
SELECT name,
@@ -176,6 +184,7 @@ def get_asset_details_for_grouped_by_category(filters):
"from_date": filters.from_date,
"company": filters.company,
"asset": filters.get("asset"),
"finance_book": filters.get("finance_book"),
},
as_dict=1,
)

View File

@@ -154,8 +154,8 @@ def get_payment_entries(filters):
select
"Payment Entry" as payment_document, name as payment_entry,
reference_no, reference_date as ref_date,
if(paid_to=%(account)s, received_amount, 0) as debit,
if(paid_from=%(account)s, paid_amount, 0) as credit,
if(paid_to=%(account)s, received_amount_after_tax, 0) as debit,
if(paid_from=%(account)s, paid_amount_after_tax, 0) as credit,
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
from `tabPayment Entry`

View File

@@ -0,0 +1,44 @@
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.query_reports["Cheques and Deposits Incorrectly cleared"] = {
filters: [
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
reqd: 1,
default: frappe.defaults.get_user_default("Company"),
},
{
fieldname: "account",
label: __("Bank Account"),
fieldtype: "Link",
options: "Account",
default: frappe.defaults.get_user_default("Company")
? locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]
: "",
reqd: 1,
get_query: function () {
var company = frappe.query_report.get_filter_value("company");
return {
query: "erpnext.controllers.queries.get_account_list",
filters: [
["Account", "account_type", "in", "Bank, Cash"],
["Account", "is_group", "=", 0],
["Account", "disabled", "=", 0],
["Account", "company", "=", company],
],
};
},
},
{
fieldname: "report_date",
label: __("Date"),
fieldtype: "Date",
default: frappe.datetime.get_today(),
reqd: 1,
},
],
};

View File

@@ -0,0 +1,29 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2024-07-30 17:20:07.570971",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"letterhead": null,
"modified": "2024-07-30 17:20:07.570971",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cheques and Deposits Incorrectly cleared",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Payment Entry",
"report_name": "Cheques and Deposits Incorrectly cleared",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
},
{
"role": "Accounts Manager"
}
]
}

View File

@@ -0,0 +1,153 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _, qb
from frappe.query_builder import CustomFunction
from frappe.query_builder.custom import ConstantColumn
def execute(filters=None):
columns = get_columns()
data = build_data(filters)
return columns, data
def build_payment_entry_dict(row: dict) -> dict:
row_dict = frappe._dict()
row_dict.update(
{
"payment_document": row.get("doctype"),
"payment_entry": row.get("name"),
"posting_date": row.get("posting_date"),
"clearance_date": row.get("clearance_date"),
}
)
if row.get("payment_type") == "Receive" and row.get("party_type") in ["Customer", "Supplier"]:
row_dict.update(
{
"debit": row.get("amount"),
"credit": 0,
}
)
else:
row_dict.update(
{
"debit": 0,
"credit": row.get("amount"),
}
)
return row_dict
def build_journal_entry_dict(row: dict) -> dict:
row_dict = frappe._dict()
row_dict.update(
{
"payment_document": row.get("doctype"),
"payment_entry": row.get("name"),
"posting_date": row.get("posting_date"),
"clearance_date": row.get("clearance_date"),
"debit": row.get("debit_in_account_currency"),
"credit": row.get("credit_in_account_currency"),
}
)
return row_dict
def build_data(filters):
vouchers = get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filters)
data = []
for x in vouchers:
if x.doctype == "Payment Entry":
data.append(build_payment_entry_dict(x))
elif x.doctype == "Journal Entry":
data.append(build_journal_entry_dict(x))
return data
def get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filters):
je = qb.DocType("Journal Entry")
jea = qb.DocType("Journal Entry Account")
doctype_name = ConstantColumn("Journal Entry")
journals = (
qb.from_(je)
.inner_join(jea)
.on(je.name == jea.parent)
.select(
doctype_name.as_("doctype"),
je.name,
jea.debit_in_account_currency,
jea.credit_in_account_currency,
je.posting_date,
je.clearance_date,
)
.where(
je.docstatus.eq(1)
& jea.account.eq(filters.account)
& je.posting_date.gt(filters.report_date)
& je.clearance_date.lte(filters.report_date)
& (je.is_opening.isnull() | je.is_opening.eq("No"))
)
.run(as_dict=1)
)
ifelse = CustomFunction("IF", ["condition", "then", "else"])
pe = qb.DocType("Payment Entry")
doctype_name = ConstantColumn("Payment Entry")
payments = (
qb.from_(pe)
.select(
doctype_name.as_("doctype"),
pe.name,
ifelse(pe.paid_from.eq(filters.account), pe.paid_amount, pe.received_amount).as_("amount"),
pe.payment_type,
pe.party_type,
pe.posting_date,
pe.clearance_date,
)
.where(
pe.docstatus.eq(1)
& (pe.paid_from.eq(filters.account) | pe.paid_to.eq(filters.account))
& pe.posting_date.gt(filters.report_date)
& pe.clearance_date.lte(filters.report_date)
)
.run(as_dict=1)
)
return journals + payments
def get_columns():
return [
{
"fieldname": "payment_document",
"label": _("Payment Document Type"),
"fieldtype": "Data",
"width": 220,
},
{
"fieldname": "payment_entry",
"label": _("Payment Document"),
"fieldtype": "Dynamic Link",
"options": "payment_document",
"width": 220,
},
{
"fieldname": "debit",
"label": _("Debit"),
"fieldtype": "Currency",
"options": "account_currency",
"width": 120,
},
{
"fieldname": "credit",
"label": _("Credit"),
"fieldtype": "Currency",
"options": "account_currency",
"width": 120,
},
{"fieldname": "posting_date", "label": _("Posting Date"), "fieldtype": "Date", "width": 110},
{"fieldname": "clearance_date", "label": _("Clearance Date"), "fieldtype": "Date", "width": 110},
]

View File

@@ -767,8 +767,12 @@ def get_daily_depr_amount(asset, row, schedule_idx, amount):
every_year_depr = amount / total_years
depr_period_start_date = add_days(
get_last_day(add_months(row.depreciation_start_date, row.frequency_of_depreciation * -1)), 1
)
year_start_date = add_years(
row.depreciation_start_date, (row.frequency_of_depreciation * schedule_idx) // 12
depr_period_start_date, ((row.frequency_of_depreciation * schedule_idx) // 12)
)
year_end_date = add_days(add_years(year_start_date, 1), -1)

View File

@@ -97,7 +97,7 @@ class Supplier(TransactionBase):
elif supp_master_name == "Naming Series":
set_name_by_naming_series(self)
else:
self.name = set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self)
set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self)
def on_update(self):
self.create_primary_contact()

View File

@@ -0,0 +1,62 @@
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.query_reports["Item-wise Purchase History"] = {
filters: [
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1,
},
{
fieldname: "from_date",
reqd: 1,
label: __("From Date"),
fieldtype: "Date",
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
},
{
fieldname: "to_date",
reqd: 1,
default: frappe.datetime.get_today(),
label: __("To Date"),
fieldtype: "Date",
},
{
fieldname: "item_group",
label: __("Item Group"),
fieldtype: "Link",
options: "Item Group",
},
{
fieldname: "item_code",
label: __("Item"),
fieldtype: "Link",
options: "Item",
get_query: () => {
return {
query: "erpnext.controllers.queries.item_query",
};
},
},
{
fieldname: "supplier",
label: __("Supplier"),
fieldtype: "Link",
options: "Supplier",
},
],
formatter: function (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
let format_fields = ["received_qty", "billed_amt"];
if (format_fields.includes(column.fieldname) && data && data[column.fieldname] > 0) {
value = "<span style='color:green;'>" + value + "</span>";
}
return value;
},
};

View File

@@ -1,30 +1,30 @@
{
"add_total_row": 1,
"apply_user_permissions": 1,
"creation": "2013-05-03 14:55:53",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 3,
"is_standard": "Yes",
"modified": "2017-02-24 20:08:57.446613",
"modified_by": "Administrator",
"module": "Buying",
"name": "Item-wise Purchase History",
"owner": "Administrator",
"query": "select\n po_item.item_code as \"Item Code:Link/Item:120\",\n\tpo_item.item_name as \"Item Name::120\",\n po_item.item_group as \"Item Group:Link/Item Group:120\",\n\tpo_item.description as \"Description::150\",\n\tpo_item.qty as \"Qty:Float:100\",\n\tpo_item.uom as \"UOM:Link/UOM:80\",\n\tpo_item.base_rate as \"Rate:Currency:120\",\n\tpo_item.base_amount as \"Amount:Currency:120\",\n\tpo.name as \"Purchase Order:Link/Purchase Order:120\",\n\tpo.transaction_date as \"Transaction Date:Date:140\",\n\tpo.supplier as \"Supplier:Link/Supplier:130\",\n sup.supplier_name as \"Supplier Name::150\",\n\tpo_item.project as \"Project:Link/Project:130\",\n\tifnull(po_item.received_qty, 0) as \"Received Qty:Float:120\",\n\tpo.company as \"Company:Link/Company:\"\nfrom\n\t`tabPurchase Order` po, `tabPurchase Order Item` po_item, `tabSupplier` sup\nwhere\n\tpo.name = po_item.parent and po.supplier = sup.name and po.docstatus = 1\norder by po.name desc",
"ref_doctype": "Purchase Order",
"report_name": "Item-wise Purchase History",
"report_type": "Query Report",
"add_total_row": 1,
"creation": "2013-05-03 14:55:53",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 5,
"is_standard": "Yes",
"modified": "2024-06-19 12:12:15.418799",
"modified_by": "Administrator",
"module": "Buying",
"name": "Item-wise Purchase History",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Purchase Order",
"report_name": "Item-wise Purchase History",
"report_type": "Script Report",
"roles": [
{
"role": "Stock User"
},
},
{
"role": "Purchase Manager"
},
},
{
"role": "Purchase User"
}
]
]
}

View File

@@ -0,0 +1,276 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.utils import flt
from frappe.utils.nestedset import get_descendants_of
def execute(filters=None):
filters = frappe._dict(filters or {})
if filters.from_date > filters.to_date:
frappe.throw(_("From Date cannot be greater than To Date"))
columns = get_columns(filters)
data = get_data(filters)
chart_data = get_chart_data(data)
return columns, data, None, chart_data
def get_columns(filters):
return [
{
"label": _("Item Code"),
"fieldtype": "Link",
"fieldname": "item_code",
"options": "Item",
"width": 120,
},
{
"label": _("Item Name"),
"fieldtype": "Data",
"fieldname": "item_name",
"width": 140,
},
{
"label": _("Item Group"),
"fieldtype": "Link",
"fieldname": "item_group",
"options": "Item Group",
"width": 120,
},
{
"label": _("Description"),
"fieldtype": "Data",
"fieldname": "description",
"width": 140,
},
{
"label": _("Quantity"),
"fieldtype": "Float",
"fieldname": "quantity",
"width": 120,
},
{
"label": _("UOM"),
"fieldtype": "Link",
"fieldname": "uom",
"options": "UOM",
"width": 90,
},
{
"label": _("Rate"),
"fieldname": "rate",
"fieldtype": "Currency",
"options": "currency",
"width": 120,
},
{
"label": _("Amount"),
"fieldname": "amount",
"fieldtype": "Currency",
"options": "currency",
"width": 120,
},
{
"label": _("Purchase Order"),
"fieldtype": "Link",
"fieldname": "purchase_order",
"options": "Purchase Order",
"width": 160,
},
{
"label": _("Transaction Date"),
"fieldtype": "Date",
"fieldname": "transaction_date",
"width": 110,
},
{
"label": _("Supplier"),
"fieldtype": "Link",
"fieldname": "supplier",
"options": "Supplier",
"width": 100,
},
{
"label": _("Supplier Name"),
"fieldtype": "Data",
"fieldname": "supplier_name",
"width": 140,
},
{
"label": _("Supplier Group"),
"fieldtype": "Link",
"fieldname": "supplier_group",
"options": "Supplier Group",
"width": 120,
},
{
"label": _("Project"),
"fieldtype": "Link",
"fieldname": "project",
"options": "Project",
"width": 100,
},
{
"label": _("Received Quantity"),
"fieldtype": "Float",
"fieldname": "received_qty",
"width": 150,
},
{
"label": _("Billed Amount"),
"fieldtype": "Currency",
"fieldname": "billed_amt",
"options": "currency",
"width": 120,
},
{
"label": _("Company"),
"fieldtype": "Link",
"fieldname": "company",
"options": "Company",
"width": 100,
},
{
"label": _("Currency"),
"fieldtype": "Link",
"fieldname": "currency",
"options": "Currency",
"hidden": 1,
},
]
def get_data(filters):
data = []
company_list = get_descendants_of("Company", filters.get("company"))
company_list.append(filters.get("company"))
supplier_details = get_supplier_details()
item_details = get_item_details()
purchase_order_records = get_purchase_order_details(company_list, filters)
for record in purchase_order_records:
supplier_record = supplier_details.get(record.supplier)
item_record = item_details.get(record.item_code)
row = {
"item_code": record.get("item_code"),
"item_name": item_record.get("item_name"),
"item_group": item_record.get("item_group"),
"description": record.get("description"),
"quantity": record.get("qty"),
"uom": record.get("uom"),
"rate": record.get("base_rate"),
"amount": record.get("base_amount"),
"purchase_order": record.get("name"),
"transaction_date": record.get("transaction_date"),
"supplier": record.get("supplier"),
"supplier_name": supplier_record.get("supplier_name"),
"supplier_group": supplier_record.get("supplier_group"),
"project": record.get("project"),
"received_qty": flt(record.get("received_qty")),
"billed_amt": flt(record.get("billed_amt")),
"company": record.get("company"),
}
row["currency"] = frappe.get_cached_value("Company", row["company"], "default_currency")
data.append(row)
return data
def get_supplier_details():
details = frappe.get_all("Supplier", fields=["name", "supplier_name", "supplier_group"])
supplier_details = {}
for d in details:
supplier_details.setdefault(
d.name,
frappe._dict({"supplier_name": d.supplier_name, "supplier_group": d.supplier_group}),
)
return supplier_details
def get_item_details():
details = frappe.db.get_all("Item", fields=["name", "item_name", "item_group"])
item_details = {}
for d in details:
item_details.setdefault(d.name, frappe._dict({"item_name": d.item_name, "item_group": d.item_group}))
return item_details
def get_purchase_order_details(company_list, filters):
db_po = frappe.qb.DocType("Purchase Order")
db_po_item = frappe.qb.DocType("Purchase Order Item")
query = (
frappe.qb.from_(db_po)
.inner_join(db_po_item)
.on(db_po_item.parent == db_po.name)
.select(
db_po.name,
db_po.supplier,
db_po.transaction_date,
db_po.project,
db_po.company,
db_po_item.item_code,
db_po_item.description,
db_po_item.qty,
db_po_item.uom,
db_po_item.base_rate,
db_po_item.base_amount,
db_po_item.received_qty,
(db_po_item.billed_amt * db_po.conversion_rate).as_("billed_amt"),
)
.where(db_po.docstatus == 1)
.where(db_po.company.isin(tuple(company_list)))
)
for field in ("item_code", "item_group"):
if filters.get(field):
query = query.where(db_po_item[field] == filters[field])
if filters.get("from_date"):
query = query.where(db_po.transaction_date >= filters.from_date)
if filters.get("to_date"):
query = query.where(db_po.transaction_date <= filters.to_date)
if filters.get("supplier"):
query = query.where(db_po.supplier == filters.supplier)
return query.run(as_dict=1)
def get_chart_data(data):
item_wise_purchase_map = {}
labels, datapoints = [], []
for row in data:
item_key = row.get("item_code")
if item_key not in item_wise_purchase_map:
item_wise_purchase_map[item_key] = 0
item_wise_purchase_map[item_key] = flt(item_wise_purchase_map[item_key]) + flt(row.get("amount"))
item_wise_purchase_map = {
item: value
for item, value in (sorted(item_wise_purchase_map.items(), key=lambda i: i[1], reverse=True))
}
for key in item_wise_purchase_map:
labels.append(key)
datapoints.append(item_wise_purchase_map[key])
return {
"data": {
"labels": labels[:30], # show max of 30 items in chart
"datasets": [{"name": _("Total Purchase Amount"), "values": datapoints[:30]}],
},
"type": "bar",
"fieldtype": "Currency",
}

View File

@@ -1334,6 +1334,12 @@ class AccountsController(TransactionBase):
# Cancelling existing exchange gain/loss journals is handled during the `on_cancel` event.
# see accounts/utils.py:cancel_exchange_gain_loss_journal()
if self.docstatus == 1:
if dimensions_dict is None:
dimensions_dict = frappe._dict()
active_dimensions = get_dimensions()[0]
for dim in active_dimensions:
dimensions_dict[dim.fieldname] = self.get(dim.fieldname)
if self.get("doctype") == "Journal Entry":
# 'args' is populated with exchange gain/loss account and the amount to be booked.
# These are generated by Sales/Purchase Invoice during reconciliation and advance allocation.

View File

@@ -185,7 +185,8 @@
{
"fieldname": "expected_closing",
"fieldtype": "Date",
"label": "Expected Closing Date"
"label": "Expected Closing Date",
"no_copy": 1
},
{
"fieldname": "section_break_14",
@@ -357,6 +358,7 @@
"fieldname": "transaction_date",
"fieldtype": "Date",
"label": "Opportunity Date",
"no_copy": 1,
"oldfieldname": "transaction_date",
"oldfieldtype": "Date",
"reqd": 1,
@@ -388,6 +390,7 @@
"fieldname": "first_response_time",
"fieldtype": "Duration",
"label": "First Response Time",
"no_copy": 1,
"read_only": 1
},
{
@@ -622,7 +625,7 @@
"icon": "fa fa-info-sign",
"idx": 195,
"links": [],
"modified": "2022-10-13 12:42:21.545636",
"modified": "2024-08-20 04:12:29.095761",
"modified_by": "Administrator",
"module": "CRM",
"name": "Opportunity",

View File

@@ -2053,6 +2053,55 @@ class TestWorkOrder(FrappeTestCase):
"BOM",
)
def test_disassemby_order(self):
fg_item = "Test Disassembly Item"
source_warehouse = "Stores - _TC"
raw_materials = ["Test Disassembly RM Item 1", "Test Disassembly RM Item 2"]
make_item(fg_item, {"is_stock_item": 1})
for item in raw_materials:
make_item(item, {"is_stock_item": 1})
test_stock_entry.make_stock_entry(
item_code=item,
target=source_warehouse,
qty=1,
basic_rate=100,
)
make_bom(item=fg_item, source_warehouse=source_warehouse, raw_materials=raw_materials)
wo = make_wo_order_test_record(
item=fg_item,
qty=1,
source_warehouse=source_warehouse,
skip_transfer=1,
)
stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 1))
for row in stock_entry.items:
if row.item_code in raw_materials:
row.s_warehouse = source_warehouse
stock_entry.submit()
wo.reload()
self.assertEqual(wo.status, "Completed")
stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Disassemble", 1))
stock_entry.save()
self.assertEqual(stock_entry.purpose, "Disassemble")
for row in stock_entry.items:
if row.item_code == fg_item:
self.assertTrue(row.s_warehouse)
self.assertFalse(row.t_warehouse)
else:
self.assertFalse(row.s_warehouse)
self.assertTrue(row.t_warehouse)
stock_entry.submit()
def make_operation(**kwargs):
kwargs = frappe._dict(kwargs)
@@ -2370,6 +2419,7 @@ def make_wo_order_test_record(**args):
wo_order.batch_size = args.batch_size or 0
if args.source_warehouse:
wo_order.source_warehouse = args.source_warehouse
for item in wo_order.get("required_items"):
item.source_warehouse = args.source_warehouse

View File

@@ -183,13 +183,30 @@ frappe.ui.form.on("Work Order", {
}
}
if (frm.doc.status == "Completed") {
if (frm.doc.__onload.backflush_raw_materials_based_on == "Material Transferred for Manufacture") {
frm.add_custom_button(
__("BOM"),
() => {
frm.trigger("make_bom");
},
__("Create")
);
}
}
if (
frm.doc.status == "Completed" &&
frm.doc.__onload.backflush_raw_materials_based_on == "Material Transferred for Manufacture"
frm.doc.docstatus === 1 &&
["Closed", "Completed"].includes(frm.doc.status) &&
frm.doc.produced_qty > 0
) {
frm.add_custom_button(__("Create BOM"), () => {
frm.trigger("make_bom");
});
frm.add_custom_button(
__("Disassembly Order"),
() => {
frm.trigger("make_disassembly_order");
},
__("Create")
);
}
frm.trigger("add_custom_button_to_return_components");
@@ -337,6 +354,23 @@ frappe.ui.form.on("Work Order", {
});
},
make_disassembly_order(frm) {
erpnext.work_order
.show_prompt_for_qty_input(frm, "Disassemble")
.then((data) => {
return frappe.xcall("erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", {
work_order_id: frm.doc.name,
purpose: "Disassemble",
qty: data.qty,
target_warehouse: data.target_warehouse,
});
})
.then((stock_entry) => {
frappe.model.sync(stock_entry);
frappe.set_route("Form", stock_entry.doctype, stock_entry.name);
});
},
show_progress_for_items: function (frm) {
var bars = [];
var message = "";
@@ -745,6 +779,10 @@ erpnext.work_order = {
get_max_transferable_qty: (frm, purpose) => {
let max = 0;
if (purpose === "Disassemble") {
return flt(frm.doc.produced_qty);
}
if (frm.doc.skip_transfer) {
max = flt(frm.doc.qty) - flt(frm.doc.produced_qty);
} else {
@@ -759,15 +797,38 @@ erpnext.work_order = {
show_prompt_for_qty_input: function (frm, purpose) {
let max = this.get_max_transferable_qty(frm, purpose);
let fields = [
{
fieldtype: "Float",
label: __("Qty for {0}", [__(purpose)]),
fieldname: "qty",
description: __("Max: {0}", [max]),
default: max,
},
];
if (purpose === "Disassemble") {
fields.push({
fieldtype: "Link",
options: "Warehouse",
fieldname: "target_warehouse",
label: __("Target Warehouse"),
default: frm.doc.source_warehouse || frm.doc.wip_warehouse,
get_query() {
return {
filters: {
company: frm.doc.company,
is_group: 0,
},
};
},
});
}
return new Promise((resolve, reject) => {
frappe.prompt(
{
fieldtype: "Float",
label: __("Qty for {0}", [__(purpose)]),
fieldname: "qty",
description: __("Max: {0}", [max]),
default: max,
},
fields,
(data) => {
max += (frm.doc.qty * (frm.doc.__onload.overproduction_percentage || 0.0)) / 100;

View File

@@ -1359,7 +1359,7 @@ def set_work_order_ops(name):
@frappe.whitelist()
def make_stock_entry(work_order_id, purpose, qty=None):
def make_stock_entry(work_order_id, purpose, qty=None, target_warehouse=None):
work_order = frappe.get_doc("Work Order", work_order_id)
if not frappe.db.get_value("Warehouse", work_order.wip_warehouse, "is_group"):
wip_warehouse = work_order.wip_warehouse
@@ -1389,9 +1389,16 @@ def make_stock_entry(work_order_id, purpose, qty=None):
stock_entry.to_warehouse = work_order.fg_warehouse
stock_entry.project = work_order.project
if purpose == "Disassemble":
stock_entry.from_warehouse = work_order.fg_warehouse
stock_entry.to_warehouse = target_warehouse or work_order.source_warehouse
stock_entry.set_stock_entry_type()
stock_entry.get_items()
stock_entry.set_serial_no_batch_for_finished_good()
if purpose != "Disassemble":
stock_entry.set_serial_no_batch_for_finished_good()
return stock_entry.as_dict()

View File

@@ -372,3 +372,5 @@ erpnext.patches.v15_0.update_asset_repair_field_in_stock_entry
erpnext.patches.v15_0.update_total_number_of_booked_depreciations
erpnext.patches.v15_0.do_not_use_batchwise_valuation
erpnext.patches.v15_0.drop_index_posting_datetime_from_sle
erpnext.patches.v15_0.add_disassembly_order_stock_entry_type #1
erpnext.patches.v15_0.set_standard_stock_entry_type

View File

@@ -0,0 +1,13 @@
import frappe
def execute():
if not frappe.db.exists("Stock Entry Type", "Disassemble"):
frappe.get_doc(
{
"doctype": "Stock Entry Type",
"name": "Disassemble",
"purpose": "Disassemble",
"is_standard": 1,
}
).insert(ignore_permissions=True)

View File

@@ -0,0 +1,17 @@
import frappe
def execute():
for stock_entry_type in [
"Material Issue",
"Material Receipt",
"Material Transfer",
"Material Transfer for Manufacture",
"Material Consumption for Manufacture",
"Manufacture",
"Repack",
"Send to Subcontractor",
"Disassemble",
]:
if frappe.db.exists("Stock Entry Type", stock_entry_type):
frappe.db.set_value("Stock Entry Type", stock_entry_type, "is_standard", 1)

View File

@@ -1421,12 +1421,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
let show = cint(this.frm.doc.discount_amount) ||
((this.frm.doc.taxes || []).filter(function(d) {return d.included_in_print_rate===1}).length);
if(frappe.meta.get_docfield(cur_frm.doctype, "net_total"))
if(this.frm.doc.doctype && frappe.meta.get_docfield(this.frm.doc.doctype, "net_total")) {
this.frm.toggle_display("net_total", show);
}
if(frappe.meta.get_docfield(cur_frm.doctype, "base_net_total"))
if(this.frm.doc.doctype && frappe.meta.get_docfield(this.frm.doc.doctype, "base_net_total")) {
this.frm.toggle_display("base_net_total", (show && (me.frm.doc.currency != company_currency)));
}
}
change_grid_labels(company_currency) {

View File

@@ -308,13 +308,15 @@
"fetch_from": "customer_primary_contact.mobile_no",
"fieldname": "mobile_no",
"fieldtype": "Read Only",
"label": "Mobile No"
"label": "Mobile No",
"options": "Mobile"
},
{
"fetch_from": "customer_primary_contact.email_id",
"fieldname": "email_id",
"fieldtype": "Read Only",
"label": "Email Id"
"label": "Email Id",
"options": "Email"
},
{
"fieldname": "column_break_26",
@@ -592,7 +594,7 @@
"link_fieldname": "party"
}
],
"modified": "2024-05-08 18:03:20.716169",
"modified": "2024-06-17 03:24:59.612974",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",
@@ -677,4 +679,4 @@
"states": [],
"title_field": "customer_name",
"track_changes": 1
}
}

View File

@@ -105,7 +105,7 @@ class Customer(TransactionBase):
elif cust_master_name == "Naming Series":
set_name_by_naming_series(self)
else:
self.name = set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self)
set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self)
def get_customer_name(self):
if frappe.db.get_value("Customer", self.customer_name) and not frappe.flags.in_import:

View File

@@ -66,29 +66,54 @@ def install(country=None):
"parent_item_group": _("All Item Groups"),
},
# Stock Entry Type
{"doctype": "Stock Entry Type", "name": "Material Issue", "purpose": "Material Issue"},
{"doctype": "Stock Entry Type", "name": "Material Receipt", "purpose": "Material Receipt"},
{
"doctype": "Stock Entry Type",
"name": "Material Issue",
"purpose": "Material Issue",
"is_standard": 1,
},
{
"doctype": "Stock Entry Type",
"name": "Material Receipt",
"purpose": "Material Receipt",
"is_standard": 1,
},
{
"doctype": "Stock Entry Type",
"name": "Material Transfer",
"purpose": "Material Transfer",
"is_standard": 1,
},
{"doctype": "Stock Entry Type", "name": "Manufacture", "purpose": "Manufacture"},
{"doctype": "Stock Entry Type", "name": "Repack", "purpose": "Repack"},
{
"doctype": "Stock Entry Type",
"name": "Manufacture",
"purpose": "Manufacture",
"is_standard": 1,
},
{
"doctype": "Stock Entry Type",
"name": "Repack",
"purpose": "Repack",
"is_standard": 1,
},
{"doctype": "Stock Entry Type", "name": "Disassemble", "purpose": "Disassemble", "is_standard": 1},
{
"doctype": "Stock Entry Type",
"name": "Send to Subcontractor",
"purpose": "Send to Subcontractor",
"is_standard": 1,
},
{
"doctype": "Stock Entry Type",
"name": "Material Transfer for Manufacture",
"purpose": "Material Transfer for Manufacture",
"is_standard": 1,
},
{
"doctype": "Stock Entry Type",
"name": "Material Consumption for Manufacture",
"purpose": "Material Consumption for Manufacture",
"is_standard": 1,
},
# territory: with two default territories, one for home country and one named Rest of the World
{

View File

@@ -267,6 +267,7 @@ class MaterialRequest(BuyingController):
mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance")
for d in self.get("items"):
precision = d.precision("ordered_qty")
if d.name in mr_items:
if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"):
d.ordered_qty = flt(mr_items_ordered_qty.get(d.name))
@@ -276,14 +277,14 @@ class MaterialRequest(BuyingController):
(d.qty + (d.qty * (mr_qty_allowance / 100))), d.precision("ordered_qty")
)
if d.ordered_qty and d.ordered_qty > allowed_qty:
if d.ordered_qty and flt(d.ordered_qty, precision) > flt(allowed_qty, precision):
frappe.throw(
_(
"The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than allowed requested quantity {2} for Item {3}"
).format(d.ordered_qty, d.parent, allowed_qty, d.item_code)
)
elif d.ordered_qty and d.ordered_qty > d.stock_qty:
elif d.ordered_qty and flt(d.ordered_qty, precision) > flt(d.stock_qty, precision):
frappe.throw(
_(
"The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than requested quantity {2} for Item {3}"

View File

@@ -1075,6 +1075,7 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
if item.billed_amt and item.amount:
adjusted_amt = flt(item.billed_amt) - flt(item.amount)
adjusted_amt = adjusted_amt * flt(pr_doc.conversion_rate)
item.db_set("rate_difference_with_purchase_invoice", adjusted_amt, update_modified=False)
percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6)

View File

@@ -3,7 +3,7 @@
import frappe
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, cint, cstr, flt, getdate, nowtime, today
from frappe.utils import add_days, cint, cstr, flt, get_datetime, getdate, nowtime, today
from pypika import functions as fn
import erpnext
@@ -3592,6 +3592,71 @@ class TestPurchaseReceipt(FrappeTestCase):
inter_transfer_dn.cancel()
frappe.db.set_single_value("Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 1)
def test_sles_with_same_posting_datetime_and_creation(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.stock.report.stock_balance.stock_balance import execute
item_code = "Test Item for SLE with same posting datetime and creation"
create_item(item_code)
pr = make_purchase_receipt(
item_code=item_code,
qty=10,
rate=100,
posting_date="2023-11-06",
posting_time="00:00:00",
)
sr = make_stock_entry(
item_code=item_code,
source=pr.items[0].warehouse,
qty=10,
posting_date="2023-11-07",
posting_time="14:28:0.330404",
)
sle = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": sr.doctype, "voucher_no": sr.name, "item_code": sr.items[0].item_code},
"name",
)
sle_doc = frappe.get_doc("Stock Ledger Entry", sle)
sle_doc.db_set("creation", "2023-11-07 14:28:01.208930")
sle_doc.reload()
self.assertEqual(get_datetime(sle_doc.creation), get_datetime("2023-11-07 14:28:01.208930"))
sr = make_stock_entry(
item_code=item_code,
target=pr.items[0].warehouse,
qty=50,
posting_date="2023-11-07",
posting_time="14:28:0.920825",
)
sle = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": sr.doctype, "voucher_no": sr.name, "item_code": sr.items[0].item_code},
"name",
)
sle_doc = frappe.get_doc("Stock Ledger Entry", sle)
sle_doc.db_set("creation", "2023-11-07 14:28:01.044561")
sle_doc.reload()
self.assertEqual(get_datetime(sle_doc.creation), get_datetime("2023-11-07 14:28:01.044561"))
pr.repost_future_sle_and_gle(force=True)
columns, data = execute(
filters=frappe._dict(
{"item_code": item_code, "warehouse": pr.items[0].warehouse, "company": pr.company}
)
)
self.assertEqual(data[0].get("bal_qty"), 50.0)
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -127,7 +127,7 @@
"label": "Purpose",
"oldfieldname": "purpose",
"oldfieldtype": "Select",
"options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor",
"options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor\nDisassemble",
"read_only": 1,
"search_index": 1
},
@@ -143,7 +143,7 @@
"reqd": 1
},
{
"depends_on": "eval:in_list([\"Material Transfer for Manufacture\", \"Manufacture\", \"Material Consumption for Manufacture\"], doc.purpose)",
"depends_on": "eval:in_list([\"Material Transfer for Manufacture\", \"Manufacture\", \"Material Consumption for Manufacture\", \"Disassemble\"], doc.purpose)",
"fieldname": "work_order",
"fieldtype": "Link",
"label": "Work Order",
@@ -242,7 +242,7 @@
},
{
"default": "0",
"depends_on": "eval:in_list([\"Material Issue\", \"Material Transfer\", \"Manufacture\", \"Repack\", \"Send to Subcontractor\", \"Material Transfer for Manufacture\", \"Material Consumption for Manufacture\"], doc.purpose)",
"depends_on": "eval:in_list([\"Material Issue\", \"Material Transfer\", \"Manufacture\", \"Repack\", \"Send to Subcontractor\", \"Material Transfer for Manufacture\", \"Material Consumption for Manufacture\", \"Disassemble\"], doc.purpose)",
"fieldname": "from_bom",
"fieldtype": "Check",
"label": "From BOM",
@@ -697,7 +697,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2024-08-13 19:02:42.386955",
"modified": "2024-08-13 19:05:42.386955",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry",

View File

@@ -132,6 +132,7 @@ class StockEntry(StockController):
"Manufacture",
"Repack",
"Send to Subcontractor",
"Disassemble",
]
remarks: DF.Text | None
sales_invoice_no: DF.Link | None
@@ -337,6 +338,7 @@ class StockEntry(StockController):
"Repack",
"Send to Subcontractor",
"Material Consumption for Manufacture",
"Disassemble",
]
if self.purpose not in valid_purposes:
@@ -616,6 +618,7 @@ class StockEntry(StockController):
"Manufacture",
"Material Transfer for Manufacture",
"Material Consumption for Manufacture",
"Disassemble",
):
# check if work order is entered
@@ -983,7 +986,7 @@ class StockEntry(StockController):
def set_stock_entry_type(self):
if self.purpose:
self.stock_entry_type = frappe.get_cached_value(
"Stock Entry Type", {"purpose": self.purpose}, "name"
"Stock Entry Type", {"purpose": self.purpose, "is_standard": 1}, "name"
)
def set_purpose_for_stock_entry(self):
@@ -1703,11 +1706,63 @@ class StockEntry(StockController):
},
)
def get_items_for_disassembly(self):
"""Get items for Disassembly Order"""
if not self.work_order:
frappe.throw(_("The Work Order is mandatory for Disassembly Order"))
items = self.get_items_from_manufacture_entry()
s_warehouse = ""
if self.work_order:
s_warehouse = frappe.db.get_value("Work Order", self.work_order, "fg_warehouse")
for row in items:
child_row = self.append("items", {})
for field, value in row.items():
if value is not None:
child_row.set(field, value)
child_row.s_warehouse = (self.from_warehouse or s_warehouse) if row.is_finished_item else ""
child_row.t_warehouse = self.to_warehouse if not row.is_finished_item else ""
child_row.is_finished_item = 0 if row.is_finished_item else 1
def get_items_from_manufacture_entry(self):
return frappe.get_all(
"Stock Entry",
fields=[
"`tabStock Entry Detail`.`item_code`",
"`tabStock Entry Detail`.`item_name`",
"`tabStock Entry Detail`.`description`",
"`tabStock Entry Detail`.`qty`",
"`tabStock Entry Detail`.`transfer_qty`",
"`tabStock Entry Detail`.`stock_uom`",
"`tabStock Entry Detail`.`uom`",
"`tabStock Entry Detail`.`basic_rate`",
"`tabStock Entry Detail`.`conversion_factor`",
"`tabStock Entry Detail`.`is_finished_item`",
"`tabStock Entry Detail`.`batch_no`",
"`tabStock Entry Detail`.`serial_no`",
"`tabStock Entry Detail`.`use_serial_batch_fields`",
],
filters=[
["Stock Entry", "purpose", "=", "Manufacture"],
["Stock Entry", "work_order", "=", self.work_order],
["Stock Entry", "docstatus", "=", 1],
["Stock Entry Detail", "docstatus", "=", 1],
],
order_by="`tabStock Entry Detail`.`idx` desc, `tabStock Entry Detail`.`is_finished_item` desc",
)
@frappe.whitelist()
def get_items(self):
self.set("items", [])
self.validate_work_order()
if self.purpose == "Disassemble":
return self.get_items_for_disassembly()
if not self.posting_date or not self.posting_time:
frappe.throw(_("Posting date and posting time is mandatory"))

View File

@@ -7,7 +7,8 @@
"engine": "InnoDB",
"field_order": [
"purpose",
"add_to_transit"
"add_to_transit",
"is_standard"
],
"fields": [
{
@@ -16,7 +17,7 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Purpose",
"options": "\nMaterial Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor",
"options": "\nMaterial Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor\nDisassemble",
"reqd": 1,
"set_only_once": 1
},
@@ -26,13 +27,21 @@
"fieldname": "add_to_transit",
"fieldtype": "Check",
"label": "Add to Transit"
},
{
"default": "0",
"fieldname": "is_standard",
"fieldtype": "Check",
"label": "Is Standard",
"read_only": 1
}
],
"links": [],
"modified": "2024-07-08 08:41:19.385020",
"modified": "2024-08-24 16:00:22.696958",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Type",
"naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{

View File

@@ -2,7 +2,7 @@
# For license information, please see license.txt
# import frappe
import frappe
from frappe.model.document import Document
@@ -16,6 +16,7 @@ class StockEntryType(Document):
from frappe.types import DF
add_to_transit: DF.Check
is_standard: DF.Check
purpose: DF.Literal[
"",
"Material Issue",
@@ -26,9 +27,25 @@ class StockEntryType(Document):
"Manufacture",
"Repack",
"Send to Subcontractor",
"Disassemble",
]
# end: auto-generated types
def validate(self):
self.validate_standard_type()
if self.add_to_transit and self.purpose != "Material Transfer":
self.add_to_transit = 0
def validate_standard_type(self):
if self.is_standard and self.name not in [
"Material Issue",
"Material Receipt",
"Material Transfer",
"Material Transfer for Manufacture",
"Material Consumption for Manufacture",
"Manufacture",
"Repack",
"Send to Subcontractor",
"Disassemble",
]:
frappe.throw(f"Stock Entry Type {self.name} cannot be set as standard")

View File

@@ -3,6 +3,33 @@
import unittest
import frappe
class TestStockEntryType(unittest.TestCase):
pass
def test_stock_entry_type_non_standard(self):
stock_entry_type = "Test Manufacturing"
doc = frappe.get_doc(
{
"doctype": "Stock Entry Type",
"__newname": stock_entry_type,
"purpose": "Manufacture",
"is_standard": 1,
}
)
self.assertRaises(frappe.ValidationError, doc.insert)
def test_stock_entry_type_is_standard(self):
for stock_entry_type in [
"Material Issue",
"Material Receipt",
"Material Transfer",
"Material Transfer for Manufacture",
"Material Consumption for Manufacture",
"Manufacture",
"Repack",
"Send to Subcontractor",
]:
self.assertTrue(frappe.db.get_value("Stock Entry Type", stock_entry_type, "is_standard"))

View File

@@ -352,7 +352,8 @@
{
"fieldname": "posting_datetime",
"fieldtype": "Datetime",
"label": "Posting Datetime"
"label": "Posting Datetime",
"search_index": 1
}
],
"hide_toolbar": 1,
@@ -361,7 +362,7 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-06-27 16:23:18.820049",
"modified": "2024-08-27 09:28:03.961443",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Ledger Entry",

View File

@@ -351,3 +351,4 @@ def on_doctype_update():
frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"])
frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"])
frappe.db.add_index("Stock Ledger Entry", ["warehouse", "item_code"], "item_warehouse")
frappe.db.add_index("Stock Ledger Entry", ["posting_datetime", "creation"])

View File

@@ -1187,7 +1187,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
qty=5,
posting_date="2021-01-01",
rate=10,
posting_time="02:00:00.1234",
posting_time="02:00:00",
)
time.sleep(3)
@@ -1199,7 +1199,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
qty=100,
rate=10,
posting_date="2021-01-01",
posting_time="02:00:00",
posting_time="02:00:00.1234",
)
sle = frappe.get_all(

View File

@@ -168,7 +168,7 @@ def get_stock_ledger_entries_for_batch_bundle(filters):
& (sle.has_batch_no == 1)
& (sle.posting_date <= filters["to_date"])
)
.groupby(batch_package.batch_no, batch_package.warehouse)
.groupby(sle.voucher_no, batch_package.batch_no, batch_package.warehouse)
.orderby(sle.item_code, sle.warehouse)
)

View File

@@ -183,7 +183,7 @@ class SerialBatchBundle:
}
if self.sle.actual_qty < 0 and self.is_material_transfer():
values_to_update["valuation_rate"] = sn_doc.avg_rate
values_to_update["valuation_rate"] = flt(sn_doc.avg_rate)
if not frappe.db.get_single_value(
"Stock Settings", "do_not_update_serial_batch_on_creation_of_auto_bundle"

View File

@@ -1543,7 +1543,7 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc
and (
posting_datetime {operator} %(posting_datetime)s
)
order by posting_datetime desc, creation desc
order by posting_date desc, posting_time desc, creation desc
limit 1
for update""",
{
@@ -1636,7 +1636,7 @@ def get_stock_ledger_entries(
where item_code = %(item_code)s
and is_cancelled = 0
{conditions}
order by posting_datetime {order}, creation {order}
order by posting_date {order}, posting_time {order}, creation {order}
{limit} {for_update}""".format(
conditions=conditions,
limit=limit or "",
@@ -1753,7 +1753,7 @@ def get_valuation_rate(
AND valuation_rate >= 0
AND is_cancelled = 0
AND NOT (voucher_no = %s AND voucher_type = %s)
order by posting_datetime desc, name desc limit 1""",
order by posting_date desc, posting_time desc, name desc limit 1""",
(item_code, warehouse, voucher_no, voucher_type),
):
return flt(last_valuation_rate[0][0])
@@ -2004,7 +2004,7 @@ def get_future_sle_with_negative_qty(args):
and posting_datetime >= %(posting_datetime)s
and is_cancelled = 0
and qty_after_transaction < 0
order by posting_datetime asc
order by posting_date asc, posting_time asc
limit 1
""",
args,
@@ -2018,14 +2018,14 @@ def get_future_sle_with_negative_batch_qty(args):
with batch_ledger as (
select
posting_date, posting_time, posting_datetime, voucher_type, voucher_no,
sum(actual_qty) over (order by posting_datetime, creation) as cumulative_total
sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
from `tabStock Ledger Entry`
where
item_code = %(item_code)s
and warehouse = %(warehouse)s
and batch_no=%(batch_no)s
and is_cancelled = 0
order by posting_datetime, creation
order by posting_date, posting_time, creation
)
select * from batch_ledger
where