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

chore: release v15
This commit is contained in:
Deepesh Garg 2024-01-03 11:29:39 +05:30 committed by GitHub
commit d2fdce007e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 938 additions and 493 deletions

View File

@ -137,7 +137,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
"erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
args: {
bank_account: frm.doc.bank_account,
till_date: frm.doc.bank_statement_from_date,
till_date: frappe.datetime.add_days(frm.doc.bank_statement_from_date, -1)
},
callback: (response) => {
frm.set_value("account_opening_balance", response.message);

View File

@ -1,457 +1,152 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"actions": [],
"autoname": "naming_series:",
"beta": 0,
"creation": "2018-06-18 16:51:49.994750",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"naming_series",
"user",
"date",
"from_time",
"time",
"expense",
"custody",
"returns",
"outstanding_amount",
"payments",
"net_amount",
"amended_from"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "POS-CLO-",
"fieldname": "naming_series",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_global_search": 1,
"in_list_view": 0,
"in_standard_filter": 1,
"label": "Series",
"length": 0,
"no_copy": 0,
"options": "POS-CLO-",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "user",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "User",
"length": 0,
"no_copy": 0,
"options": "User",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Today",
"fieldname": "date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 1,
"label": "Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "from_time",
"fieldtype": "Time",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 1,
"label": "From Time",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "time",
"fieldtype": "Time",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 1,
"label": "To Time",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0.00",
"fieldname": "expense",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Expense",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Expense"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0.00",
"fieldname": "custody",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Custody",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"label": "Custody"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0.00",
"fieldname": "returns",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Returns",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "2",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"precision": "2"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0.00",
"fieldname": "outstanding_amount",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Outstanding Amount",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0.0",
"fieldname": "payments",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Payments",
"length": 0,
"no_copy": 0,
"options": "Cashier Closing Payments",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "Cashier Closing Payments"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "net_amount",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Net Amount",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "amended_from",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Amended From",
"length": 0,
"no_copy": 1,
"options": "Cashier Closing",
"permlevel": 0,
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 1,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-02-19 08:35:24.157327",
"links": [],
"modified": "2023-12-28 13:15:46.858427",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cashier Closing",
"name_case": "",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}
"states": [],
"track_changes": 1
}

View File

@ -21,6 +21,7 @@
"against_voucher_type",
"against_voucher",
"voucher_type",
"voucher_subtype",
"voucher_no",
"voucher_detail_no",
"project",
@ -278,13 +279,18 @@
"fieldtype": "Currency",
"label": "Credit Amount in Transaction Currency",
"options": "transaction_currency"
},
{
"fieldname": "voucher_subtype",
"fieldtype": "Small Text",
"label": "Voucher Subtype"
}
],
"icon": "fa fa-list",
"idx": 1,
"in_create": 1,
"links": [],
"modified": "2023-08-16 21:38:44.072267",
"modified": "2023-12-18 15:38:14.006208",
"modified_by": "Administrator",
"module": "Accounts",
"name": "GL Entry",

View File

@ -39,6 +39,8 @@ class GLEntry(Document):
account: DF.Link | None
account_currency: DF.Link | None
against: DF.Text | None
against_link: DF.DynamicLink | None
against_type: DF.Link | None
against_voucher: DF.DynamicLink | None
against_voucher_type: DF.Link | None
company: DF.Link | None
@ -66,6 +68,7 @@ class GLEntry(Document):
transaction_exchange_rate: DF.Float
voucher_detail_no: DF.Data | None
voucher_no: DF.DynamicLink | None
voucher_subtype: DF.SmallText | None
voucher_type: DF.Link | None
# end: auto-generated types

View File

@ -765,7 +765,7 @@ def get_pos_reserved_qty(item_code, warehouse):
reserved_qty = (
frappe.qb.from_(p_inv)
.from_(p_item)
.select(Sum(p_item.qty).as_("qty"))
.select(Sum(p_item.stock_qty).as_("stock_qty"))
.where(
(p_inv.name == p_item.parent)
& (IfNull(p_inv.consolidated_invoice, "") == "")
@ -775,7 +775,7 @@ def get_pos_reserved_qty(item_code, warehouse):
)
).run(as_dict=True)
return reserved_qty[0].qty or 0 if reserved_qty else 0
return flt(reserved_qty[0].stock_qty) if reserved_qty else 0
@frappe.whitelist()

View File

@ -440,7 +440,7 @@ def reconcile(doc: None | str = None) -> None:
# Update the parent doc about the exception
frappe.db.rollback()
traceback = frappe.get_traceback()
traceback = frappe.get_traceback(with_context=True)
if traceback:
message = "Traceback: <br>" + traceback
frappe.db.set_value("Process Payment Reconciliation Log", log, "error_log", message)

View File

@ -1135,11 +1135,17 @@ class PurchaseInvoice(BuyingController):
)
assets = frappe.db.get_all(
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
"Asset",
filters={"purchase_invoice": self.name, "item_code": item.item_code},
fields=["name", "asset_quantity"],
)
for asset in assets:
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate))
frappe.db.set_value(
"Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate) * asset.asset_quantity
)
frappe.db.set_value(
"Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate) * asset.asset_quantity
)
def make_stock_adjustment_entry(
self, gl_entries, item, voucher_wise_stock_value, account_currency

View File

@ -1985,6 +1985,26 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
self.assertEqual(pi.items[0].cost_center, "_Test Cost Center Buying - _TC")
def test_debit_note_without_item(self):
pi = make_purchase_invoice(item_name="_Test Item", qty=10, do_not_submit=True)
pi.items[0].item_code = ""
pi.save()
self.assertFalse(pi.items[0].item_code)
pi.submit()
return_pi = make_purchase_invoice(
item_name="_Test Item",
is_return=1,
return_against=pi.name,
qty=-10,
do_not_save=True,
)
return_pi.items[0].item_code = ""
return_pi.save()
return_pi.submit()
self.assertEqual(return_pi.docstatus, 1)
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(
@ -2121,6 +2141,7 @@ def make_purchase_invoice(**args):
"items",
{
"item_code": args.item or args.item_code or "_Test Item",
"item_name": args.item_name,
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": args.qty or 5,
"received_qty": args.received_qty or 0,

View File

@ -43,7 +43,7 @@ def start_payment_ledger_repost(docname=None):
except Exception as e:
frappe.db.rollback()
traceback = frappe.get_traceback()
traceback = frappe.get_traceback(with_context=True)
if traceback:
message = "Traceback: <br>" + traceback
frappe.db.set_value(repost_doc.doctype, repost_doc.name, "repost_error_log", message)

View File

@ -586,6 +586,8 @@ class SalesInvoice(SellingController):
"Serial and Batch Bundle",
)
self.delete_auto_created_batches()
def update_status_updater_args(self):
if cint(self.update_stock):
self.status_updater.append(

View File

@ -1414,10 +1414,11 @@ class TestSalesInvoice(FrappeTestCase):
def test_serialized_cancel(self):
si = self.test_serialized()
si.cancel()
si.reload()
serial_nos = get_serial_nos_from_bundle(si.get("items")[0].serial_and_batch_bundle)
si.cancel()
self.assertEqual(
frappe.db.get_value("Serial No", serial_nos[0], "warehouse"), "_Test Warehouse - _TC"
)

View File

@ -81,6 +81,7 @@
"warehouse",
"target_warehouse",
"quality_inspection",
"pick_serial_and_batch",
"serial_and_batch_bundle",
"batch_no",
"incoming_rate",
@ -897,12 +898,18 @@
"options": "Serial and Batch Bundle",
"print_hide": 1,
"search_index": 1
},
{
"depends_on": "eval:parent.update_stock === 1",
"fieldname": "pick_serial_and_batch",
"fieldtype": "Button",
"label": "Pick Serial / Batch No"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2023-11-14 18:34:10.479329",
"modified": "2023-12-29 13:03:14.121298",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",

View File

@ -148,13 +148,13 @@
{
"fieldname": "additional_discount_percentage",
"fieldtype": "Percent",
"label": "Additional DIscount Percentage"
"label": "Additional Discount Percentage"
},
{
"collapsible": 1,
"fieldname": "additional_discount_amount",
"fieldtype": "Currency",
"label": "Additional DIscount Amount"
"label": "Additional Discount Amount"
},
{
"collapsible": 1,
@ -267,7 +267,7 @@
"link_fieldname": "subscription"
}
],
"modified": "2023-09-18 17:48:21.900252",
"modified": "2023-12-28 17:20:42.687789",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Subscription",

View File

@ -356,18 +356,20 @@ class Subscription(Document):
self,
from_date: Optional[Union[str, datetime.date]] = None,
to_date: Optional[Union[str, datetime.date]] = None,
posting_date: Optional[Union[str, datetime.date]] = None,
) -> Document:
"""
Creates a `Invoice` for the `Subscription`, updates `self.invoices` and
saves the `Subscription`.
Backwards compatibility
"""
return self.create_invoice(from_date=from_date, to_date=to_date)
return self.create_invoice(from_date=from_date, to_date=to_date, posting_date=posting_date)
def create_invoice(
self,
from_date: Optional[Union[str, datetime.date]] = None,
to_date: Optional[Union[str, datetime.date]] = None,
posting_date: Optional[Union[str, datetime.date]] = None,
) -> Document:
"""
Creates a `Invoice`, submits it and returns it
@ -385,11 +387,13 @@ class Subscription(Document):
invoice = frappe.new_doc(self.invoice_document_type)
invoice.company = company
invoice.set_posting_time = 1
invoice.posting_date = (
self.current_invoice_start
if self.generate_invoice_at == "Beginning of the current subscription period"
else self.current_invoice_end
)
if self.generate_invoice_at == "Beginning of the current subscription period":
invoice.posting_date = self.current_invoice_start
elif self.generate_invoice_at == "Days before the current subscription period":
invoice.posting_date = posting_date or self.current_invoice_start
else:
invoice.posting_date = self.current_invoice_end
invoice.cost_center = self.cost_center
@ -413,6 +417,7 @@ class Subscription(Document):
# Subscription is better suited for service items. I won't update `update_stock`
# for that reason
items_list = self.get_items_from_plans(self.plans, is_prorate())
for item in items_list:
item["cost_center"] = self.cost_center
invoice.append("items", item)
@ -556,7 +561,7 @@ class Subscription(Document):
if not self.is_current_invoice_generated(
self.current_invoice_start, self.current_invoice_end
) and self.can_generate_new_invoice(posting_date):
self.generate_invoice()
self.generate_invoice(posting_date=posting_date)
self.update_subscription_period(add_days(self.current_invoice_end, 1))
if self.cancel_at_period_end and (

View File

@ -25,11 +25,26 @@ frappe.query_reports["Asset Depreciations and Balances"] = {
"default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
"reqd": 1
},
{
"fieldname":"group_by",
"label": __("Group By"),
"fieldtype": "Select",
"options": ["Asset Category", "Asset"],
"default": "Asset Category",
},
{
"fieldname":"asset_category",
"label": __("Asset Category"),
"fieldtype": "Link",
"options": "Asset Category"
}
"options": "Asset Category",
"depends_on": "eval: doc.group_by == 'Asset Category'",
},
{
"fieldname":"asset",
"label": __("Asset"),
"fieldtype": "Link",
"options": "Asset",
"depends_on": "eval: doc.group_by == 'Asset'",
},
]
}

View File

@ -14,10 +14,17 @@ def execute(filters=None):
def get_data(filters):
if filters.get("group_by") == "Asset Category":
return get_group_by_asset_category_data(filters)
elif filters.get("group_by") == "Asset":
return get_group_by_asset_data(filters)
def get_group_by_asset_category_data(filters):
data = []
asset_categories = get_asset_categories(filters)
assets = get_assets(filters)
asset_categories = get_asset_categories_for_grouped_by_category(filters)
assets = get_assets_for_grouped_by_category(filters)
for asset_category in asset_categories:
row = frappe._dict()
@ -38,6 +45,7 @@ def get_data(filters):
if asset["asset_category"] == asset_category.get("asset_category", "")
)
)
row.accumulated_depreciation_as_on_to_date = (
flt(row.accumulated_depreciation_as_on_from_date)
+ flt(row.depreciation_amount_during_the_period)
@ -57,7 +65,7 @@ def get_data(filters):
return data
def get_asset_categories(filters):
def get_asset_categories_for_grouped_by_category(filters):
condition = ""
if filters.get("asset_category"):
condition += " and asset_category = %(asset_category)s"
@ -116,7 +124,105 @@ def get_asset_categories(filters):
)
def get_assets(filters):
def get_asset_details_for_grouped_by_category(filters):
condition = ""
if filters.get("asset"):
condition += " and name = %(asset)s"
return frappe.db.sql(
"""
SELECT name,
ifnull(sum(case when purchase_date < %(from_date)s then
case when ifnull(disposal_date, 0) = 0 or disposal_date >= %(from_date)s then
gross_purchase_amount
else
0
end
else
0
end), 0) as cost_as_on_from_date,
ifnull(sum(case when purchase_date >= %(from_date)s then
gross_purchase_amount
else
0
end), 0) as cost_of_new_purchase,
ifnull(sum(case when ifnull(disposal_date, 0) != 0
and disposal_date >= %(from_date)s
and disposal_date <= %(to_date)s then
case when status = "Sold" then
gross_purchase_amount
else
0
end
else
0
end), 0) as cost_of_sold_asset,
ifnull(sum(case when ifnull(disposal_date, 0) != 0
and disposal_date >= %(from_date)s
and disposal_date <= %(to_date)s then
case when status = "Scrapped" then
gross_purchase_amount
else
0
end
else
0
end), 0) as cost_of_scrapped_asset
from `tabAsset`
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {}
group by name
""".format(
condition
),
{
"to_date": filters.to_date,
"from_date": filters.from_date,
"company": filters.company,
"asset": filters.get("asset"),
},
as_dict=1,
)
def get_group_by_asset_data(filters):
data = []
asset_details = get_asset_details_for_grouped_by_category(filters)
assets = get_assets_for_grouped_by_asset(filters)
for asset_detail in asset_details:
row = frappe._dict()
# row.asset_category = asset_category
row.update(asset_detail)
row.cost_as_on_to_date = (
flt(row.cost_as_on_from_date)
+ flt(row.cost_of_new_purchase)
- flt(row.cost_of_sold_asset)
- flt(row.cost_of_scrapped_asset)
)
row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
row.accumulated_depreciation_as_on_to_date = (
flt(row.accumulated_depreciation_as_on_from_date)
+ flt(row.depreciation_amount_during_the_period)
- flt(row.depreciation_eliminated_during_the_period)
)
row.net_asset_value_as_on_from_date = flt(row.cost_as_on_from_date) - flt(
row.accumulated_depreciation_as_on_from_date
)
row.net_asset_value_as_on_to_date = flt(row.cost_as_on_to_date) - flt(
row.accumulated_depreciation_as_on_to_date
)
data.append(row)
return data
def get_assets_for_grouped_by_category(filters):
condition = ""
if filters.get("asset_category"):
condition = " and a.asset_category = '{}'".format(filters.get("asset_category"))
@ -178,15 +284,93 @@ def get_assets(filters):
)
def get_assets_for_grouped_by_asset(filters):
condition = ""
if filters.get("asset"):
condition = " and a.name = '{}'".format(filters.get("asset"))
return frappe.db.sql(
"""
SELECT results.name as asset,
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
from (SELECT a.name as name,
ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
gle.debit
else
0
end), 0) as accumulated_depreciation_as_on_from_date,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
gle.debit
else
0
end), 0) as depreciation_eliminated_during_the_period,
ifnull(sum(case when gle.posting_date >= %(from_date)s and gle.posting_date <= %(to_date)s
and (ifnull(a.disposal_date, 0) = 0 or gle.posting_date <= a.disposal_date) then
gle.debit
else
0
end), 0) as depreciation_amount_during_the_period
from `tabGL Entry` gle
join `tabAsset` a on
gle.against_voucher = a.name
join `tabAsset Category Account` aca on
aca.parent = a.asset_category and aca.company_name = %(company)s
join `tabCompany` company on
company.name = %(company)s
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0}
group by a.name
union
SELECT a.name as name,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then
0
else
a.opening_accumulated_depreciation
end), 0) as accumulated_depreciation_as_on_from_date,
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
a.opening_accumulated_depreciation
else
0
end), 0) as depreciation_eliminated_during_the_period,
0 as depreciation_amount_during_the_period
from `tabAsset` a
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0}
group by a.name) as results
group by results.name
""".format(
condition
),
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
as_dict=1,
)
def get_columns(filters):
return [
{
"label": _("Asset Category"),
"fieldname": "asset_category",
"fieldtype": "Link",
"options": "Asset Category",
"width": 120,
},
columns = []
if filters.get("group_by") == "Asset Category":
columns.append(
{
"label": _("Asset Category"),
"fieldname": "asset_category",
"fieldtype": "Link",
"options": "Asset Category",
"width": 120,
}
)
elif filters.get("group_by") == "Asset":
columns.append(
{
"label": _("Asset"),
"fieldname": "asset",
"fieldtype": "Link",
"options": "Asset",
"width": 120,
}
)
columns += [
{
"label": _("Cost as on") + " " + formatdate(filters.day_before_from_date),
"fieldname": "cost_as_on_from_date",
@ -254,3 +438,5 @@ def get_columns(filters):
"width": 200,
},
]
return columns

View File

@ -2,7 +2,44 @@
// License: GNU General Public License v3. See license.txt
frappe.query_reports["Budget Variance Report"] = {
"filters": [
"filters": get_filters(),
"formatter": function (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (column.fieldname.includes(__("variance"))) {
if (data[column.fieldname] < 0) {
value = "<span style='color:red'>" + value + "</span>";
}
else if (data[column.fieldname] > 0) {
value = "<span style='color:green'>" + value + "</span>";
}
}
return value;
}
}
function get_filters() {
function get_dimensions() {
let result = [];
frappe.call({
method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions",
args: {
'with_cost_center_and_project': true
},
async: false,
callback: function(r) {
if(!r.exc) {
result = r.message[0].map(elem => elem.document_type);
}
}
});
return result;
}
let budget_against_options = get_dimensions();
let filters = [
{
fieldname: "from_fiscal_year",
label: __("From Fiscal Year"),
@ -44,9 +81,13 @@ frappe.query_reports["Budget Variance Report"] = {
fieldname: "budget_against",
label: __("Budget Against"),
fieldtype: "Select",
options: ["Cost Center", "Project"],
options: budget_against_options,
default: "Cost Center",
reqd: 1,
get_data: function() {
console.log(this.options);
return ["Emacs", "Rocks"];
},
on_change: function() {
frappe.query_report.set_filter_value("budget_against_filter", []);
frappe.query_report.refresh();
@ -71,24 +112,8 @@ frappe.query_reports["Budget Variance Report"] = {
fieldtype: "Check",
default: 0,
},
],
"formatter": function (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
]
if (column.fieldname.includes(__("variance"))) {
if (data[column.fieldname] < 0) {
value = "<span style='color:red'>" + value + "</span>";
}
else if (data[column.fieldname] > 0) {
value = "<span style='color:green'>" + value + "</span>";
}
}
return value;
}
return filters;
}
erpnext.dimension_filters.forEach((dimension) => {
frappe.query_reports["Budget Variance Report"].filters[4].options.push(dimension["document_type"]);
});

View File

@ -3,7 +3,7 @@
import frappe
from frappe import _, scrub
from frappe import _, qb, scrub
from frappe.utils import getdate, nowdate
@ -38,7 +38,6 @@ class PartyLedgerSummaryReport(object):
"""
Additional Columns for 'User Permission' based access control
"""
from frappe import qb
if self.filters.party_type == "Customer":
self.territories = frappe._dict({})
@ -365,13 +364,29 @@ class PartyLedgerSummaryReport(object):
def get_party_adjustment_amounts(self):
conditions = self.prepare_conditions()
income_or_expense = (
"Expense Account" if self.filters.party_type == "Customer" else "Income Account"
account_type = "Expense Account" if self.filters.party_type == "Customer" else "Income Account"
income_or_expense_accounts = frappe.db.get_all(
"Account", filters={"account_type": account_type, "company": self.filters.company}, pluck="name"
)
invoice_dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit"
reverse_dr_or_cr = "credit" if self.filters.party_type == "Customer" else "debit"
round_off_account = frappe.get_cached_value("Company", self.filters.company, "round_off_account")
gl = qb.DocType("GL Entry")
if not income_or_expense_accounts:
# prevent empty 'in' condition
income_or_expense_accounts.append("")
accounts_query = (
qb.from_(gl)
.select(gl.voucher_type, gl.voucher_no)
.where(
(gl.account.isin(income_or_expense_accounts))
& (gl.posting_date.gte(self.filters.from_date))
& (gl.posting_date.lte(self.filters.to_date))
)
)
gl_entries = frappe.db.sql(
"""
select
@ -381,16 +396,15 @@ class PartyLedgerSummaryReport(object):
where
docstatus < 2 and is_cancelled = 0
and (voucher_type, voucher_no) in (
select voucher_type, voucher_no from `tabGL Entry` gle, `tabAccount` acc
where acc.name = gle.account and acc.account_type = '{income_or_expense}'
and gle.posting_date between %(from_date)s and %(to_date)s and gle.docstatus < 2
{accounts_query}
) and (voucher_type, voucher_no) in (
select voucher_type, voucher_no from `tabGL Entry` gle
where gle.party_type=%(party_type)s and ifnull(party, '') != ''
and gle.posting_date between %(from_date)s and %(to_date)s and gle.docstatus < 2 {conditions}
)
""".format(
conditions=conditions, income_or_expense=income_or_expense
""".format(
accounts_query=accounts_query,
conditions=conditions,
),
self.filters,
as_dict=True,
@ -414,7 +428,7 @@ class PartyLedgerSummaryReport(object):
elif gle.party:
parties.setdefault(gle.party, 0)
parties[gle.party] += gle.get(reverse_dr_or_cr) - gle.get(invoice_dr_or_cr)
elif frappe.get_cached_value("Account", gle.account, "account_type") == income_or_expense:
elif frappe.get_cached_value("Account", gle.account, "account_type") == account_type:
accounts.setdefault(gle.account, 0)
accounts[gle.account] += gle.get(invoice_dr_or_cr) - gle.get(reverse_dr_or_cr)
else:

View File

@ -211,7 +211,13 @@ def get_data(
ignore_accumulated_values_for_fy,
)
accumulate_values_into_parents(accounts, accounts_by_name, period_list)
out = prepare_data(accounts, balance_must_be, period_list, company_currency)
out = prepare_data(
accounts,
balance_must_be,
period_list,
company_currency,
accumulated_values=filters.accumulated_values,
)
out = filter_out_zero_value_rows(out, parent_children_map)
if out and total:
@ -270,7 +276,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name, period_list):
) + d.get("opening_balance", 0.0)
def prepare_data(accounts, balance_must_be, period_list, company_currency):
def prepare_data(accounts, balance_must_be, period_list, company_currency, accumulated_values):
data = []
year_start_date = period_list[0]["year_start_date"].strftime("%Y-%m-%d")
year_end_date = period_list[-1]["year_end_date"].strftime("%Y-%m-%d")
@ -310,8 +316,14 @@ def prepare_data(accounts, balance_must_be, period_list, company_currency):
has_value = True
total += flt(row[period.key])
row["has_value"] = has_value
row["total"] = total
if accumulated_values:
# when 'accumulated_values' is enabled, periods have running balance.
# so, last period will have the net amount.
row["has_value"] = has_value
row["total"] = flt(d.get(period_list[-1].key, 0.0), 3)
else:
row["has_value"] = has_value
row["total"] = total
data.append(row)
return data

View File

@ -52,6 +52,11 @@ frappe.query_reports["General Ledger"] = {
frappe.query_report.set_filter_value('group_by', "Group by Voucher (Consolidated)");
}
},
{
"fieldname":"against_voucher_no",
"label": __("Against Voucher No"),
"fieldtype": "Data",
},
{
"fieldtype": "Break",
},

View File

@ -200,7 +200,7 @@ def get_gl_entries(filters, accounting_dimensions):
"""
select
name as gl_entry, posting_date, account, party_type, party,
voucher_type, voucher_no, {dimension_fields}
voucher_type, voucher_subtype, voucher_no, {dimension_fields}
cost_center, project, {transaction_currency_fields}
against_voucher_type, against_voucher, account_currency,
against, is_opening, creation {select_fields}
@ -238,6 +238,9 @@ def get_conditions(filters):
if filters.get("voucher_no"):
conditions.append("voucher_no=%(voucher_no)s")
if filters.get("against_voucher_no"):
conditions.append("against_voucher=%(against_voucher_no)s")
if filters.get("voucher_no_not_in"):
conditions.append("voucher_no not in %(voucher_no_not_in)s")
@ -608,6 +611,12 @@ def get_columns(filters):
columns += [
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 120},
{
"label": _("Voucher Subtype"),
"fieldname": "voucher_subtype",
"fieldtype": "Data",
"width": 180,
},
{
"label": _("Voucher No"),
"fieldname": "voucher_no",

View File

@ -82,14 +82,25 @@ def get_report_summary(
if filters.get("accumulated_in_group_company"):
period_list = get_filtered_list_for_consolidated_report(filters, period_list)
for period in period_list:
key = period if consolidated else period.key
if filters.accumulated_values:
# when 'accumulated_values' is enabled, periods have running balance.
# so, last period will have the net amount.
key = period_list[-1].key
if income:
net_income += income[-2].get(key)
net_income = income[-2].get(key)
if expense:
net_expense += expense[-2].get(key)
net_expense = expense[-2].get(key)
if net_profit_loss:
net_profit += net_profit_loss.get(key)
net_profit = net_profit_loss.get(key)
else:
for period in period_list:
key = period if consolidated else period.key
if income:
net_income += income[-2].get(key)
if expense:
net_expense += expense[-2].get(key)
if net_profit_loss:
net_profit += net_profit_loss.get(key)
if len(period_list) == 1 and periodicity == "Yearly":
profit_label = _("Profit This Year")

View File

@ -0,0 +1,94 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, getdate, today
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.financial_statements import get_period_list
from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import execute
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
class TestProfitAndLossStatement(AccountsTestMixin, FrappeTestCase):
def setUp(self):
self.create_company()
self.create_customer()
self.create_item()
def tearDown(self):
frappe.db.rollback()
def create_sales_invoice(self, qty=1, rate=150, no_payment_schedule=False, do_not_submit=False):
frappe.set_user("Administrator")
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debit_to,
posting_date=today(),
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=rate,
price_list_rate=rate,
qty=qty,
do_not_save=1,
)
si = si.save()
if not do_not_submit:
si = si.submit()
return si
def get_fiscal_year(self):
active_fy = frappe.db.get_all(
"Fiscal Year",
filters={"disabled": 0, "year_start_date": ("<=", today()), "year_end_date": (">=", today())},
)[0]
return frappe.get_doc("Fiscal Year", active_fy.name)
def get_report_filters(self):
fy = self.get_fiscal_year()
return frappe._dict(
company=self.company,
from_fiscal_year=fy.name,
to_fiscal_year=fy.name,
period_start_date=fy.year_start_date,
period_end_date=fy.year_end_date,
filter_based_on="Fiscal Year",
periodicity="Monthly",
accumulated_vallues=True,
)
def test_profit_and_loss_output_and_summary(self):
si = self.create_sales_invoice(qty=1, rate=150)
filters = self.get_report_filters()
period_list = get_period_list(
filters.from_fiscal_year,
filters.to_fiscal_year,
filters.period_start_date,
filters.period_end_date,
filters.filter_based_on,
filters.periodicity,
company=filters.company,
)
result = execute(filters)[1]
current_period = [x for x in period_list if x.from_date <= getdate() and x.to_date >= getdate()][
0
]
current_period_key = current_period.key
without_current_period = [x for x in period_list if x.key != current_period.key]
# all period except current period(whence invoice was posted), should be '0'
for acc in result:
if acc:
with self.subTest(acc=acc):
for period in without_current_period:
self.assertEqual(acc[period.key], 0)
for acc in result:
if acc:
with self.subTest(current_period_key=current_period_key):
self.assertEqual(acc[current_period_key], 150)
self.assertEqual(acc["total"], 150)

View File

@ -117,8 +117,3 @@ frappe.query_reports["Profitability Analysis"] = {
"parent_field": "parent_account",
"initial_depth": 3
}
erpnext.dimension_filters.forEach((dimension) => {
frappe.query_reports["Profitability Analysis"].filters[1].options.push(dimension["document_type"]);
});

View File

@ -225,6 +225,11 @@ class AccountsController(TransactionBase):
apply_pricing_rule_on_transaction(self)
self.set_total_in_words()
self.set_default_letter_head()
def set_default_letter_head(self):
if hasattr(self, "letter_head") and not self.letter_head:
self.letter_head = frappe.db.get_value("Company", self.company, "default_letter_head")
def init_internal_values(self):
# init all the internal values as 0 on sa
@ -874,6 +879,7 @@ class AccountsController(TransactionBase):
"project": self.get("project"),
"post_net_value": args.get("post_net_value"),
"voucher_detail_no": args.get("voucher_detail_no"),
"voucher_subtype": self.get_voucher_subtype(),
}
)
@ -927,8 +933,33 @@ class AccountsController(TransactionBase):
}
)
if not args.get("against_voucher_type") and self.get("against_voucher_type"):
gl_dict.update({"against_voucher_type": self.get("against_voucher_type")})
if not args.get("against_voucher") and self.get("against_voucher"):
gl_dict.update({"against_voucher": self.get("against_voucher")})
return gl_dict
def get_voucher_subtype(self):
voucher_subtypes = {
"Journal Entry": "voucher_type",
"Payment Entry": "payment_type",
"Stock Entry": "stock_entry_type",
"Asset Capitalization": "entry_type",
}
if self.doctype in voucher_subtypes:
return self.get(voucher_subtypes[self.doctype])
elif self.doctype == "Purchase Receipt" and self.is_return:
return "Purchase Return"
elif self.doctype == "Delivery Note" and self.is_return:
return "Sales Return"
elif (self.doctype == "Sales Invoice" and self.is_return) or self.doctype == "Purchase Invoice":
return "Credit Note"
elif (self.doctype == "Purchase Invoice" and self.is_return) or self.doctype == "Sales Invoice":
return "Debit Note"
return self.doctype
def get_value_in_transaction_currency(self, account_currency, args, field):
if account_currency == self.get("currency"):
return args.get(field + "_in_account_currency")
@ -2393,6 +2424,7 @@ def validate_taxes_and_charges(tax):
def validate_account_head(idx, account, company, context=""):
account_company = frappe.get_cached_value("Account", account, "company")
is_group = frappe.get_cached_value("Account", account, "is_group")
if account_company != company:
frappe.throw(
@ -2402,6 +2434,12 @@ def validate_account_head(idx, account, company, context=""):
title=_("Invalid Account"),
)
if is_group:
frappe.throw(
_("Row {0}: Account {1} is a Group Account").format(idx, frappe.bold(account)),
title=_("Invalid Account"),
)
def validate_cost_center(tax, doc):
if not tax.cost_center:

View File

@ -562,16 +562,17 @@ def make_return_doc(
if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return
item_details = frappe.get_cached_value(
"Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1
)
if source_doc.item_code:
item_details = frappe.get_cached_value(
"Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1
)
if not item_details.has_batch_no and not item_details.has_serial_no:
return
if not item_details.has_batch_no and not item_details.has_serial_no:
return
for qty_field in ["stock_qty", "rejected_qty"]:
if target_doc.get(qty_field):
update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field)
for qty_field in ["stock_qty", "rejected_qty"]:
if target_doc.get(qty_field):
update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field)
def update_terms(source_doc, target_doc, source_parent):
target_doc.payment_amount = -source_doc.payment_amount

View File

@ -37,6 +37,7 @@ welcome_email = "erpnext.setup.utils.welcome_email"
# setup wizard
setup_wizard_requires = "assets/erpnext/js/setup_wizard.js"
setup_wizard_stages = "erpnext.setup.setup_wizard.setup_wizard.get_setup_stages"
setup_wizard_complete = "erpnext.setup.setup_wizard.setup_wizard.setup_demo"
setup_wizard_test = "erpnext.setup.setup_wizard.test_setup_wizard.run_setup_wizard_test"
before_install = [

View File

@ -1486,3 +1486,47 @@ def make_variant_bom(source_name, bom_no, item, variant_items, target_doc=None):
)
return doc
def get_op_cost_from_sub_assemblies(bom_no, op_cost=0):
# Get operating cost from sub-assemblies
bom_items = frappe.get_all(
"BOM Item", filters={"parent": bom_no, "docstatus": 1}, fields=["bom_no"], order_by="idx asc"
)
for row in bom_items:
if not row.bom_no:
continue
if cost := frappe.get_cached_value("BOM", row.bom_no, "operating_cost_per_bom_quantity"):
op_cost += flt(cost)
get_op_cost_from_sub_assemblies(row.bom_no, op_cost)
return op_cost
def get_scrap_items_from_sub_assemblies(bom_no, company, qty, scrap_items=None):
if not scrap_items:
scrap_items = {}
bom_items = frappe.get_all(
"BOM Item",
filters={"parent": bom_no, "docstatus": 1},
fields=["bom_no", "qty"],
order_by="idx asc",
)
for row in bom_items:
if not row.bom_no:
continue
qty = flt(row.qty) * flt(qty)
items = get_bom_items_as_dict(
row.bom_no, company, qty=qty, fetch_exploded=0, fetch_scrap_items=1
)
scrap_items.update(items)
get_scrap_items_from_sub_assemblies(row.bom_no, company, qty, scrap_items)
return scrap_items

View File

@ -251,7 +251,7 @@ class BOMCreator(Document):
frappe.msgprint(_("BOMs created successfully"))
except Exception:
traceback = frappe.get_traceback()
traceback = frappe.get_traceback(with_context=True)
self.db_set(
{
"status": "Failed",

View File

@ -37,7 +37,8 @@
"oldfieldname": "item_code",
"oldfieldtype": "Link",
"options": "Item",
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "item_name",
@ -170,7 +171,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-11-14 18:35:40.856895",
"modified": "2024-01-02 13:49:36.211586",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Explosion Item",

View File

@ -273,35 +273,39 @@ class JobCard(Document):
def has_overlap(self, production_capacity, time_logs):
overlap = False
if production_capacity == 1 and len(time_logs) > 0:
if production_capacity == 1 and len(time_logs) >= 1:
return True
if not len(time_logs):
return False
# Check overlap exists or not between the overlapping time logs with the current Job Card
for row in time_logs:
count = 1
for next_row in time_logs:
if row.name == next_row.name:
continue
if (
(
get_datetime(next_row.from_time) >= get_datetime(row.from_time)
and get_datetime(next_row.from_time) <= get_datetime(row.to_time)
)
or (
get_datetime(next_row.to_time) >= get_datetime(row.from_time)
and get_datetime(next_row.to_time) <= get_datetime(row.to_time)
)
or (
get_datetime(next_row.from_time) <= get_datetime(row.from_time)
and get_datetime(next_row.to_time) >= get_datetime(row.to_time)
)
):
count += 1
if count > production_capacity:
return True
# sorting overlapping job cards as per from_time
time_logs = sorted(time_logs, key=lambda x: x.get("from_time"))
# alloted_capacity has key number starting from 1. Key number will increment by 1 if non sequential job card found
# if key number reaches/crosses to production_capacity means capacity is full and overlap error generated
# this will store last to_time of sequential job cards
alloted_capacity = {1: time_logs[0]["to_time"]}
# flag for sequential Job card found
sequential_job_card_found = False
for i in range(1, len(time_logs)):
# scanning for all Existing keys
for key in alloted_capacity.keys():
# if current Job Card from time is greater than last to_time in that key means these job card are sequential
if alloted_capacity[key] <= time_logs[i]["from_time"]:
# So update key's value with last to_time
alloted_capacity[key] = time_logs[i]["to_time"]
# flag is true as we get sequential Job Card for that key
sequential_job_card_found = True
# Immediately break so that job card to time is not added with any other key except this
break
# if sequential job card not found above means it is overlapping so increment key number to alloted_capacity
if not sequential_job_card_found:
# increment key number
key = key + 1
# for that key last to time is assigned.
alloted_capacity[key] = time_logs[i]["to_time"]
if len(alloted_capacity) >= production_capacity:
# if number of keys greater or equal to production caoacity means full capacity is utilized and we should throw overlap error
return True
return overlap
def get_time_logs(self, args, doctype, check_next_available_slot=False):

View File

@ -31,6 +31,7 @@
"job_card_excess_transfer",
"other_settings_section",
"update_bom_costs_automatically",
"set_op_cost_and_scrape_from_sub_assemblies",
"column_break_23",
"make_serial_no_batch_from_work_order"
],
@ -194,13 +195,20 @@
"fieldname": "job_card_excess_transfer",
"fieldtype": "Check",
"label": "Allow Excess Material Transfer"
},
{
"default": "0",
"description": "In the case of 'Use Multi-Level BOM' in a work order, if the user wishes to add sub-assembly costs to Finished Goods items without using a job card as well the scrap items, then this option needs to be enable.",
"fieldname": "set_op_cost_and_scrape_from_sub_assemblies",
"fieldtype": "Check",
"label": "Set Operating Cost / Scrape Items From Sub-assemblies"
}
],
"icon": "icon-wrench",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-09-13 22:09:09.401559",
"modified": "2023-12-28 16:37:44.874096",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing Settings",
@ -216,5 +224,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -32,6 +32,7 @@ class ManufacturingSettings(Document):
mins_between_operations: DF.Int
overproduction_percentage_for_sales_order: DF.Percent
overproduction_percentage_for_work_order: DF.Percent
set_op_cost_and_scrape_from_sub_assemblies: DF.Check
update_bom_costs_automatically: DF.Check
# end: auto-generated types

View File

@ -1602,6 +1602,10 @@ def make_bom(**args):
}
)
if args.operating_cost_per_bom_quantity:
bom.fg_based_operating_cost = 1
bom.operating_cost_per_bom_quantity = args.operating_cost_per_bom_quantity
for item in args.raw_materials:
item_doc = frappe.get_doc("Item", item)
bom.append(

View File

@ -920,11 +920,9 @@ class TestWorkOrder(FrappeTestCase):
"Test RM Item 2 for Scrap Item Test",
]
from_time = add_days(now(), -1)
job_cards = frappe.get_all(
"Job Card Time Log",
fields=["distinct parent as name", "docstatus"],
filters={"from_time": (">", from_time)},
order_by="creation asc",
)
@ -1731,6 +1729,93 @@ class TestWorkOrder(FrappeTestCase):
job_card2.time_logs = []
job_card2.save()
def test_op_cost_and_scrap_based_on_sub_assemblies(self):
# Make Sub Assembly BOM 1
frappe.db.set_single_value(
"Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies", 1
)
items = {
"Test Final FG Item": 0,
"Test Final SF Item 1": 0,
"Test Final SF Item 2": 0,
"Test Final RM Item 1": 100,
"Test Final RM Item 2": 200,
"Test Final Scrap Item 1": 50,
"Test Final Scrap Item 2": 60,
}
for item in items:
if not frappe.db.exists("Item", item):
item_properties = {"is_stock_item": 1, "valuation_rate": items[item]}
make_item(item_code=item, properties=item_properties),
prepare_boms_for_sub_assembly_test()
wo_order = make_wo_order_test_record(
production_item="Test Final FG Item",
qty=10,
use_multi_level_bom=1,
skip_transfer=1,
from_wip_warehouse=1,
)
se_doc = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
se_doc.save()
self.assertTrue(se_doc.additional_costs)
scrap_items = []
for item in se_doc.items:
if item.is_scrap_item:
scrap_items.append(item.item_code)
self.assertEqual(
sorted(scrap_items), sorted(["Test Final Scrap Item 1", "Test Final Scrap Item 2"])
)
for row in se_doc.additional_costs:
self.assertEqual(row.amount, 3000)
frappe.db.set_single_value(
"Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies", 0
)
def prepare_boms_for_sub_assembly_test():
if not frappe.db.exists("BOM", {"item": "Test Final SF Item 1"}):
bom = make_bom(
item="Test Final SF Item 1",
source_warehouse="Stores - _TC",
raw_materials=["Test Final RM Item 1"],
operating_cost_per_bom_quantity=100,
do_not_submit=True,
)
bom.append("scrap_items", {"item_code": "Test Final Scrap Item 1", "qty": 1})
bom.submit()
if not frappe.db.exists("BOM", {"item": "Test Final SF Item 2"}):
bom = make_bom(
item="Test Final SF Item 2",
source_warehouse="Stores - _TC",
raw_materials=["Test Final RM Item 2"],
operating_cost_per_bom_quantity=200,
do_not_submit=True,
)
bom.append("scrap_items", {"item_code": "Test Final Scrap Item 2", "qty": 1})
bom.submit()
if not frappe.db.exists("BOM", {"item": "Test Final FG Item"}):
bom = make_bom(
item="Test Final FG Item",
source_warehouse="Stores - _TC",
raw_materials=["Test Final SF Item 1", "Test Final SF Item 2"],
)
def prepare_data_for_workstation_type_check():
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
@ -1977,6 +2062,7 @@ def make_wo_order_test_record(**args):
wo_order.sales_order = args.sales_order or None
wo_order.planned_start_date = args.planned_start_date or now()
wo_order.transfer_material_against = args.transfer_material_against or "Work Order"
wo_order.from_wip_warehouse = args.from_wip_warehouse or 0
if args.source_warehouse:
for item in wo_order.get("required_items"):

View File

@ -843,7 +843,7 @@ erpnext.utils.map_current_doc = function(opts) {
freeze_message: __("Mapping {0} ...", [opts.source_doctype]),
callback: function(r) {
if(!r.exc) {
var doc = frappe.model.sync(r.message);
frappe.model.sync(r.message);
cur_frm.dirty();
cur_frm.refresh();
}
@ -870,6 +870,11 @@ erpnext.utils.map_current_doc = function(opts) {
target: opts.target,
date_field: opts.date_field || undefined,
setters: opts.setters,
data_fields: [{
fieldname: 'merge_taxes',
fieldtype: 'Check',
label: __('Merge taxes from multiple documents'),
}],
get_query: opts.get_query,
add_filters_group: 1,
allow_child_item_selection: opts.allow_child_item_selection,
@ -883,10 +888,7 @@ erpnext.utils.map_current_doc = function(opts) {
return;
}
opts.source_name = values;
if (opts.allow_child_item_selection) {
// args contains filtered child docnames
opts.args = args;
}
opts.args = args;
d.dialog.hide();
_map();
},

View File

@ -25,7 +25,7 @@ def update_itemised_tax_data(doc):
# dont even bother checking in item tax template as it contains both input and output accounts - double the tax rate
item_code = row.item_code or row.item_name
if itemised_tax.get(item_code):
for tax in itemised_tax.get(row.item_code).values():
for tax in itemised_tax.get(item_code).values():
_tax_rate = flt(tax.get("tax_rate", 0), row.precision("tax_rate"))
tax_amount += flt((row.net_amount * _tax_rate) / 100, row.precision("tax_amount"))
tax_rate += _tax_rate

View File

@ -448,7 +448,6 @@
"report_hide": 1
},
{
"default": "0",
"fieldname": "credit_limits",
"fieldtype": "Table",
"label": "Credit Limit",
@ -584,7 +583,7 @@
"link_fieldname": "party"
}
],
"modified": "2023-10-19 16:56:27.327035",
"modified": "2023-12-28 13:15:36.298369",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",

View File

@ -37,11 +37,6 @@ def get_setup_stages(args=None):
{"fn": setup_defaults, "args": args, "fail_msg": _("Failed to setup defaults")},
],
},
{
"status": _("Setting up demo data"),
"fail_msg": _("Failed to setup demo data"),
"tasks": [{"fn": setup_demo, "args": args, "fail_msg": _("Failed to setup demo data")}],
},
{
"status": _("Wrapping up"),
"fail_msg": _("Failed to login"),

View File

@ -149,6 +149,4 @@ def prepare_closing_stock_balance(name):
doc.db_set("status", "Completed")
except Exception as e:
doc.db_set("status", "Failed")
traceback = frappe.get_traceback()
frappe.log_error("Closing Stock Balance Failed", traceback, doc.doctype, doc.name)
doc.log_error(title="Closing Stock Balance Failed")

View File

@ -431,6 +431,8 @@ class DeliveryNote(SellingController):
"Serial and Batch Bundle",
)
self.delete_auto_created_batches()
def update_stock_reservation_entries(self) -> None:
"""Updates Delivered Qty in Stock Reservation Entries."""

View File

@ -1478,6 +1478,46 @@ class TestDeliveryNote(FrappeTestCase):
returned_dn.reload()
self.assertAlmostEqual(returned_dn.items[0].incoming_rate, 200.0)
def test_batch_with_non_stock_uom(self):
frappe.db.set_single_value(
"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 1
)
item = make_item(
properties={
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": "TESTBATCH.#####",
"stock_uom": "Nos",
}
)
if not frappe.db.exists("UOM Conversion Detail", {"parent": item.name, "uom": "Kg"}):
item.append("uoms", {"uom": "Kg", "conversion_factor": 5.0})
item.save()
item_code = item.name
make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=5, basic_rate=100.0)
dn = create_delivery_note(
item_code=item_code, qty=1, rate=500, warehouse="_Test Warehouse - _TC", do_not_save=True
)
dn.items[0].uom = "Kg"
dn.items[0].conversion_factor = 5.0
dn.save()
dn.submit()
self.assertEqual(dn.items[0].stock_qty, 5.0)
voucher_detail_no = dn.items[0].name
delivered_batch_qty = frappe.db.get_value(
"Serial and Batch Bundle", {"voucher_detail_no": voucher_detail_no}, "total_qty"
)
self.assertEqual(abs(delivered_batch_qty), 5.0)
frappe.db.set_single_value(
"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 0
)
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")

View File

@ -1121,8 +1121,39 @@ def get_item_wise_returned_qty(pr_doc):
)
def merge_taxes(source_taxes, target_doc):
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
update_item_wise_tax_detail,
)
existing_taxes = target_doc.get("taxes") or []
idx = 1
for tax in source_taxes:
found = False
for t in existing_taxes:
if t.account_head == tax.account_head and t.cost_center == tax.cost_center:
t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount)
t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount)
update_item_wise_tax_detail(t, tax)
found = True
if not found:
tax.charge_type = "Actual"
tax.idx = idx
idx += 1
tax.included_in_print_rate = 0
tax.dont_recompute_tax = 1
tax.row_id = ""
tax.tax_amount = tax.tax_amount_after_discount_amount
tax.base_tax_amount = tax.base_tax_amount_after_discount_amount
tax.item_wise_tax_detail = tax.item_wise_tax_detail
existing_taxes.append(tax)
target_doc.set("taxes", existing_taxes)
@frappe.whitelist()
def make_purchase_invoice(source_name, target_doc=None):
def make_purchase_invoice(source_name, target_doc=None, args=None):
from erpnext.accounts.party import get_payment_terms_template
doc = frappe.get_doc("Purchase Receipt", source_name)
@ -1139,6 +1170,10 @@ def make_purchase_invoice(source_name, target_doc=None):
)
doc.run_method("onload")
doc.run_method("set_missing_values")
if args and args.get("merge_taxes"):
merge_taxes(source.get("taxes") or [], doc)
doc.run_method("calculate_taxes_and_totals")
doc.set_payment_schedule()
@ -1202,7 +1237,11 @@ def make_purchase_invoice(source_name, target_doc=None):
if not doc.get("is_return")
else get_pending_qty(d)[0] > 0,
},
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "add_if_empty": True},
"Purchase Taxes and Charges": {
"doctype": "Purchase Taxes and Charges",
"add_if_empty": True,
"ignore": args.get("merge_taxes") if args else 0,
},
},
target_doc,
set_missing_values,

View File

@ -294,7 +294,7 @@ def repost(doc):
raise
frappe.db.rollback()
traceback = frappe.get_traceback()
traceback = frappe.get_traceback(with_context=True)
doc.log_error("Unable to repost item valuation")
message = frappe.message_log.pop() if frappe.message_log else ""

View File

@ -85,6 +85,7 @@ class SerialandBatchBundle(Document):
# end: auto-generated types
def validate(self):
self.reset_serial_batch_bundle()
self.set_batch_no()
self.validate_serial_and_batch_no()
self.validate_duplicate_serial_and_batch_no()
@ -100,6 +101,15 @@ class SerialandBatchBundle(Document):
self.set_incoming_rate()
self.calculate_qty_and_amount()
def reset_serial_batch_bundle(self):
if self.is_new() and self.amended_from:
for field in ["is_cancelled", "is_rejected"]:
if self.get(field):
self.set(field, 0)
if self.voucher_detail_no:
self.voucher_detail_no = None
def set_batch_no(self):
if self.has_serial_no and self.has_batch_no:
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
@ -483,7 +493,11 @@ class SerialandBatchBundle(Document):
if row.get("doctype") in ["Subcontracting Receipt Supplied Item"]:
qty_field = "consumed_qty"
if abs(abs(flt(self.total_qty, precision)) - abs(flt(row.get(qty_field), precision))) > 0.01:
qty = row.get(qty_field)
if qty_field == "qty" and row.get("stock_qty"):
qty = row.get("stock_qty")
if abs(abs(flt(self.total_qty, precision)) - abs(flt(qty, precision))) > 0.01:
self.throw_error_message(
f"Total quantity {abs(flt(self.total_qty))} in the Serial and Batch Bundle {bold(self.name)} does not match with the quantity {abs(flt(row.get(qty_field)))} for the Item {bold(self.item_code)} in the {self.voucher_type} # {self.voucher_no}"
)
@ -910,7 +924,11 @@ def upload_csv_file(item_code, file_path):
def get_serial_batch_from_csv(item_code, file_path):
file_path = frappe.get_site_path() + file_path
if "private" in file_path:
file_path = frappe.get_site_path() + file_path
else:
file_path = frappe.get_site_path() + "/public" + file_path
serial_nos = []
batch_nos = []

View File

@ -427,11 +427,12 @@ class TestSerialandBatchBundle(FrappeTestCase):
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
item = make_item(
"Test Serial and Batch Bundle Company Item",
properties={
"has_serial_no": 1,
"serial_no_series": "TT-SER-VAL-.#####",
}
)
},
).name
pr = make_purchase_receipt(
item_code=item,
@ -460,6 +461,26 @@ class TestSerialandBatchBundle(FrappeTestCase):
sn_doc = add_serial_batch_ledgers(entries, item_row, pr, "_Test Warehouse - _TC")
self.assertEqual(sn_doc.company, "_Test Company")
def test_auto_cancel_serial_and_batch(self):
item_code = make_item(
properties={"has_serial_no": 1, "serial_no_series": "ATC-TT-SER-VAL-.#####"}
).name
se = make_stock_entry(
item_code=item_code,
target="_Test Warehouse - _TC",
qty=5,
rate=500,
)
bundle = se.items[0].serial_and_batch_bundle
docstatus = frappe.db.get_value("Serial and Batch Bundle", bundle, "docstatus")
self.assertEqual(docstatus, 1)
se.cancel()
docstatus = frappe.db.get_value("Serial and Batch Bundle", bundle, "docstatus")
self.assertEqual(docstatus, 2)
def get_batch_from_bundle(bundle):
from erpnext.stock.serial_batch_bundle import get_batch_nos

View File

@ -25,7 +25,12 @@ from frappe.utils import (
import erpnext
from erpnext.accounts.general_ledger import process_gl_map
from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals
from erpnext.manufacturing.doctype.bom.bom import add_additional_cost, validate_bom_no
from erpnext.manufacturing.doctype.bom.bom import (
add_additional_cost,
get_op_cost_from_sub_assemblies,
get_scrap_items_from_sub_assemblies,
validate_bom_no,
)
from erpnext.setup.doctype.brand.brand import get_brand_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock.doctype.batch.batch import get_batch_qty
@ -1898,11 +1903,22 @@ class StockEntry(StockController):
def get_bom_scrap_material(self, qty):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
# item dict = { item_code: {qty, description, stock_uom} }
item_dict = (
get_bom_items_as_dict(self.bom_no, self.company, qty=qty, fetch_exploded=0, fetch_scrap_items=1)
or {}
)
if (
frappe.db.get_single_value(
"Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies"
)
and self.work_order
and frappe.get_cached_value("Work Order", self.work_order, "use_multi_level_bom")
):
item_dict = get_scrap_items_from_sub_assemblies(self.bom_no, self.company, qty)
else:
# item dict = { item_code: {qty, description, stock_uom} }
item_dict = (
get_bom_items_as_dict(
self.bom_no, self.company, qty=qty, fetch_exploded=0, fetch_scrap_items=1
)
or {}
)
for item in item_dict.values():
item.from_warehouse = ""
@ -2653,6 +2669,15 @@ def get_work_order_details(work_order, company):
def get_operating_cost_per_unit(work_order=None, bom_no=None):
operating_cost_per_unit = 0
if work_order:
if (
bom_no
and frappe.db.get_single_value(
"Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies"
)
and frappe.get_cached_value("Work Order", work_order, "use_multi_level_bom")
):
return get_op_cost_from_sub_assemblies(bom_no)
if not bom_no:
bom_no = work_order.bom_no

View File

@ -141,7 +141,7 @@ def create_material_request(material_requests):
exceptions_list.extend(frappe.local.message_log)
frappe.local.message_log = []
else:
exceptions_list.append(frappe.get_traceback())
exceptions_list.append(frappe.get_traceback(with_context=True))
mr.log_error("Unable to create material request")

View File

@ -242,6 +242,12 @@ class SerialBatchBundle:
if self.item_details.has_batch_no == 1:
self.update_batch_qty()
if self.sle.is_cancelled and self.sle.serial_and_batch_bundle:
self.cancel_serial_and_batch_bundle()
def cancel_serial_and_batch_bundle(self):
frappe.get_cached_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle).cancel()
def submit_serial_and_batch_bundle(self):
doc = frappe.get_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle)
self.validate_actual_qty(doc)

View File

@ -195,7 +195,6 @@ class TestFIFOValuation(unittest.TestCase):
total_value -= sum(q * r for q, r in consumed)
self.assertTotalQty(total_qty)
self.assertTotalValue(total_value)
self.assertGreaterEqual(total_value, 0)
class TestLIFOValuation(unittest.TestCase):

View File

@ -62,7 +62,7 @@ def retry_failed_transactions(failed_docs: list | None):
task(log.transaction_name, log.from_doctype, log.to_doctype)
except Exception as e:
frappe.db.rollback(save_point="before_creation_state")
update_log(log.name, "Failed", 1, str(frappe.get_traceback()))
update_log(log.name, "Failed", 1, str(frappe.get_traceback(with_context=True)))
else:
update_log(log.name, "Success", 1)
@ -86,7 +86,7 @@ def job(deserialized_data, from_doctype, to_doctype):
fail_count += 1
create_log(
doc_name,
str(frappe.get_traceback()),
str(frappe.get_traceback(with_context=True)),
from_doctype,
to_doctype,
status="Failed",