Merge pull request #39090 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
commit
d2fdce007e
@ -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);
|
||||
|
@ -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
|
||||
}
|
@ -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",
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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"
|
||||
)
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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 (
|
||||
|
@ -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'",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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"]);
|
||||
});
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
},
|
||||
|
@ -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",
|
||||
|
@ -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")
|
||||
|
@ -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)
|
@ -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"]);
|
||||
});
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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 = [
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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"):
|
||||
|
@ -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();
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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"),
|
||||
|
@ -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")
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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,
|
||||
|
@ -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 ""
|
||||
|
@ -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 = []
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user