From f9ab763cc8a7c592fd47e25b63b9ad1e5b32c988 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 13 Mar 2024 12:18:25 +0530 Subject: [PATCH 01/94] fix: achieved targets for sales partners (cherry picked from commit 1ac888715c2039f807297da92f268177e117165b) --- .../item_group_wise_sales_target_variance.py | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py index f2f1e4cfbaa2..d4e704a7e330 100644 --- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py +++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py @@ -209,31 +209,32 @@ def get_actual_data(filters, sales_users_or_territory_data, date_field, sales_fi parent_doc = frappe.qb.DocType(filters.get("doctype")) child_doc = frappe.qb.DocType(filters.get("doctype") + " Item") - sales_team = frappe.qb.DocType("Sales Team") - - query = ( - frappe.qb.from_(parent_doc) - .inner_join(child_doc) - .on(child_doc.parent == parent_doc.name) - .inner_join(sales_team) - .on(sales_team.parent == parent_doc.name) - .select( - child_doc.item_group, - (child_doc.stock_qty * sales_team.allocated_percentage / 100).as_("stock_qty"), - (child_doc.base_net_amount * sales_team.allocated_percentage / 100).as_("base_net_amount"), - sales_team.sales_person, - parent_doc[date_field], - ) - .where( - (parent_doc.docstatus == 1) - & (parent_doc[date_field].between(fiscal_year.year_start_date, fiscal_year.year_end_date)) - ) - ) + + query = frappe.qb.from_(parent_doc).inner_join(child_doc).on(child_doc.parent == parent_doc.name) if sales_field == "sales_person": - query = query.where(sales_team.sales_person.isin(sales_users_or_territory_data)) + sales_team = frappe.qb.DocType("Sales Team") + stock_qty = child_doc.stock_qty * sales_team.allocated_percentage / 100 + net_amount = child_doc.base_net_amount * sales_team.allocated_percentage / 100 + sales_field_col = sales_team[sales_field] + + query = query.inner_join(sales_team).on(sales_team.parent == parent_doc.name) else: - query = query.where(parent_doc[sales_field].isin(sales_users_or_territory_data)) + stock_qty = child_doc.stock_qty + net_amount = child_doc.base_net_amount + sales_field_col = parent_doc[sales_field] + + query = query.select( + child_doc.item_group, + parent_doc[date_field], + (stock_qty).as_("stock_qty"), + (net_amount).as_("base_net_amount"), + sales_field_col, + ).where( + (parent_doc.docstatus == 1) + & (parent_doc[date_field].between(fiscal_year.year_start_date, fiscal_year.year_end_date)) + & (sales_field_col.isin(sales_users_or_territory_data)) + ) return query.run(as_dict=True) From c3b8c00070cadea6e7b14c62e247623db589f1c7 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 13 Mar 2024 13:35:24 +0530 Subject: [PATCH 02/94] test: sales partner targets with invoices (cherry picked from commit 1a3ea0c108b51c3cd63501cee12e70328846bee6) --- ...ner_target_variance_based_on_item_group.py | 57 +++++++++++++++++++ ...son_target_variance_based_on_item_group.py | 40 ++++++++----- 2 files changed, 83 insertions(+), 14 deletions(-) create mode 100644 erpnext/selling/report/sales_partner_target_variance_based_on_item_group/test_sales_partner_target_variance_based_on_item_group.py diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/test_sales_partner_target_variance_based_on_item_group.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/test_sales_partner_target_variance_based_on_item_group.py new file mode 100644 index 000000000000..17186687d97e --- /dev/null +++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/test_sales_partner_target_variance_based_on_item_group.py @@ -0,0 +1,57 @@ +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import flt, nowdate + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.utils import get_fiscal_year +from erpnext.selling.report.sales_partner_target_variance_based_on_item_group.sales_partner_target_variance_based_on_item_group import ( + execute, +) +from erpnext.selling.report.sales_person_target_variance_based_on_item_group.test_sales_person_target_variance_based_on_item_group import ( + create_sales_target_doc, + create_target_distribution, +) + + +class TestSalesPartnerTargetVarianceBasedOnItemGroup(FrappeTestCase): + def setUp(self): + self.fiscal_year = get_fiscal_year(nowdate())[0] + + def tearDown(self): + frappe.db.rollback() + + def test_achieved_target_and_variance_for_partner(self): + # Create a Target Distribution + distribution = create_target_distribution(self.fiscal_year) + + # Create Sales Partner with targets for the current fiscal year + sales_partner = create_sales_target_doc( + "Sales Partner", "partner_name", "Sales Partner 1", self.fiscal_year, distribution.name + ) + + # Create a Sales Invoice for the Partner + si = create_sales_invoice( + rate=1000, + qty=20, + do_not_submit=True, + ) + si.sales_partner = sales_partner + si.commission_rate = 5 + si.submit() + + # Check Achieved Target and Variance for the Sales Partner + result = execute( + frappe._dict( + { + "fiscal_year": self.fiscal_year, + "doctype": "Sales Invoice", + "period": "Yearly", + "target_on": "Quantity", + } + ) + )[1] + row = frappe._dict(result[0]) + self.assertSequenceEqual( + [flt(value, 2) for value in (row.total_target, row.total_achieved, row.total_variance)], + [50, 20, -30], + ) diff --git a/erpnext/selling/report/sales_person_target_variance_based_on_item_group/test_sales_person_target_variance_based_on_item_group.py b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/test_sales_person_target_variance_based_on_item_group.py index 4ae5d2bee887..73ae6d0c8526 100644 --- a/erpnext/selling/report/sales_person_target_variance_based_on_item_group/test_sales_person_target_variance_based_on_item_group.py +++ b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/test_sales_person_target_variance_based_on_item_group.py @@ -18,17 +18,17 @@ def tearDown(self): def test_achieved_target_and_variance(self): # Create a Target Distribution - distribution = frappe.new_doc("Monthly Distribution") - distribution.distribution_id = "Target Report Distribution" - distribution.fiscal_year = self.fiscal_year - distribution.get_months() - distribution.insert() + distribution = create_target_distribution(self.fiscal_year) - # Create sales people with targets - person_1 = create_sales_person_with_target("Sales Person 1", self.fiscal_year, distribution.name) - person_2 = create_sales_person_with_target("Sales Person 2", self.fiscal_year, distribution.name) + # Create sales people with targets for the current fiscal year + person_1 = create_sales_target_doc( + "Sales Person", "sales_person_name", "Sales Person 1", self.fiscal_year, distribution.name + ) + person_2 = create_sales_target_doc( + "Sales Person", "sales_person_name", "Sales Person 2", self.fiscal_year, distribution.name + ) - # Create a Sales Order with 50-50 contribution + # Create a Sales Order with 50-50 contribution between both Sales people so = make_sales_order( rate=1000, qty=20, @@ -69,10 +69,20 @@ def test_achieved_target_and_variance(self): ) -def create_sales_person_with_target(sales_person_name, fiscal_year, distribution_id): - sales_person = frappe.new_doc("Sales Person") - sales_person.sales_person_name = sales_person_name - sales_person.append( +def create_target_distribution(fiscal_year): + distribution = frappe.new_doc("Monthly Distribution") + distribution.distribution_id = "Target Report Distribution" + distribution.fiscal_year = fiscal_year + distribution.get_months() + return distribution.insert() + + +def create_sales_target_doc( + sales_field_dt, sales_field_name, sales_field_value, fiscal_year, distribution_id +): + sales_target_doc = frappe.new_doc(sales_field_dt) + sales_target_doc.set(sales_field_name, sales_field_value) + sales_target_doc.append( "targets", { "fiscal_year": fiscal_year, @@ -81,4 +91,6 @@ def create_sales_person_with_target(sales_person_name, fiscal_year, distribution "distribution_id": distribution_id, }, ) - return sales_person.insert() + if sales_field_dt == "Sales Partner": + sales_target_doc.commission_rate = 5 + return sales_target_doc.insert() From fe3bee44e21ea2c96f30d653f7fec7040493db09 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 14 Mar 2024 12:51:51 +0530 Subject: [PATCH 03/94] fix: show correct variance for durations with no vouchers (cherry picked from commit bc78bc33f1a0a8ecac34338298ab334bd51aa8a7) --- .../item_group_wise_sales_target_variance.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py index d4e704a7e330..42bdf571738f 100644 --- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py +++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py @@ -197,6 +197,8 @@ def prepare_data( ): details[p_key] += r.get(qty_or_amount_field, 0) details[variance_key] = details.get(p_key) - details.get(target_key) + else: + details[variance_key] = details.get(p_key) - details.get(target_key) details["total_achieved"] += details.get(p_key) details["total_variance"] = details.get("total_achieved") - details.get("total_target") From ee7bd988788a49f7bda35eaa0dcb525983684cbf Mon Sep 17 00:00:00 2001 From: Danny <66078599+mmdanny89@users.noreply.github.com> Date: Mon, 18 Mar 2024 10:47:42 -0400 Subject: [PATCH 04/94] fix: pass empty string email content of pos invoice --- erpnext/selling/page/point_of_sale/pos_past_order_summary.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js index 53fedbf56cf3..96b2c051e72c 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js @@ -259,6 +259,7 @@ erpnext.PointOfSale.PastOrderSummary = class { subject: __(frm.meta.name) + ": " + doc.name, doctype: doc.doctype, name: doc.name, + content: "", send_email: 1, print_format, sender_full_name: frappe.user.full_name(), From bba1ac5735a3597dba06d9f5661fc15475a83051 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 20 Mar 2024 16:09:14 +0530 Subject: [PATCH 05/94] fix: use Text Editor for rendering tax breakup table (cherry picked from commit 1c639838736dee633c435ec728e4b7319cbd9748) # Conflicts: # erpnext/accounts/doctype/pos_invoice/pos_invoice.py # erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py # erpnext/accounts/doctype/sales_invoice/sales_invoice.py # erpnext/buying/doctype/purchase_order/purchase_order.json # erpnext/buying/doctype/purchase_order/purchase_order.py # erpnext/buying/doctype/supplier_quotation/supplier_quotation.json # erpnext/buying/doctype/supplier_quotation/supplier_quotation.py # erpnext/selling/doctype/quotation/quotation.json # erpnext/selling/doctype/quotation/quotation.py # erpnext/selling/doctype/sales_order/sales_order.py # erpnext/stock/doctype/delivery_note/delivery_note.json # erpnext/stock/doctype/delivery_note/delivery_note.py # erpnext/stock/doctype/purchase_receipt/purchase_receipt.py --- .../doctype/pos_invoice/pos_invoice.json | 4 +- .../doctype/pos_invoice/pos_invoice.py | 166 ++++++++++++++++ .../purchase_invoice/purchase_invoice.json | 4 +- .../purchase_invoice/purchase_invoice.py | 175 ++++++++++++++++ .../doctype/sales_invoice/sales_invoice.json | 6 +- .../doctype/sales_invoice/sales_invoice.py | 188 ++++++++++++++++++ .../purchase_order/purchase_order.json | 6 +- .../doctype/purchase_order/purchase_order.py | 135 +++++++++++++ .../supplier_quotation.json | 6 +- .../supplier_quotation/supplier_quotation.py | 92 +++++++++ .../selling/doctype/quotation/quotation.json | 6 +- .../selling/doctype/quotation/quotation.py | 106 ++++++++++ .../doctype/sales_order/sales_order.json | 4 +- .../doctype/sales_order/sales_order.py | 138 +++++++++++++ .../doctype/delivery_note/delivery_note.json | 6 +- .../doctype/delivery_note/delivery_note.py | 125 ++++++++++++ .../purchase_receipt/purchase_receipt.json | 4 +- .../purchase_receipt/purchase_receipt.py | 115 +++++++++++ 18 files changed, 1271 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index de333cb9e8d1..854523f10095 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -774,7 +774,7 @@ }, { "fieldname": "other_charges_calculation", - "fieldtype": "Long Text", + "fieldtype": "Text Editor", "label": "Taxes and Charges Calculation", "no_copy": 1, "oldfieldtype": "HTML", @@ -1562,7 +1562,7 @@ "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2023-11-20 12:27:12.848149", + "modified": "2024-03-20 16:00:34.268756", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 383b9dab24a2..18eee1d9562f 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -26,6 +26,172 @@ class POSInvoice(SalesInvoice): +<<<<<<< HEAD +======= + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule + from erpnext.accounts.doctype.pos_invoice_item.pos_invoice_item import POSInvoiceItem + from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail + from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import ( + SalesInvoiceAdvance, + ) + from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import ( + SalesInvoicePayment, + ) + from erpnext.accounts.doctype.sales_invoice_timesheet.sales_invoice_timesheet import ( + SalesInvoiceTimesheet, + ) + from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import ( + SalesTaxesandCharges, + ) + from erpnext.selling.doctype.sales_team.sales_team import SalesTeam + from erpnext.stock.doctype.packed_item.packed_item import PackedItem + + account_for_change_amount: DF.Link | None + additional_discount_percentage: DF.Float + address_display: DF.SmallText | None + advances: DF.Table[SalesInvoiceAdvance] + against_income_account: DF.SmallText | None + allocate_advances_automatically: DF.Check + amended_from: DF.Link | None + amount_eligible_for_commission: DF.Currency + apply_discount_on: DF.Literal["", "Grand Total", "Net Total"] + auto_repeat: DF.Link | None + base_change_amount: DF.Currency + base_discount_amount: DF.Currency + base_grand_total: DF.Currency + base_in_words: DF.Data | None + base_net_total: DF.Currency + base_paid_amount: DF.Currency + base_rounded_total: DF.Currency + base_rounding_adjustment: DF.Currency + base_total: DF.Currency + base_total_taxes_and_charges: DF.Currency + base_write_off_amount: DF.Currency + campaign: DF.Link | None + cash_bank_account: DF.Link | None + change_amount: DF.Currency + commission_rate: DF.Float + company: DF.Link + company_address: DF.Link | None + company_address_display: DF.SmallText | None + consolidated_invoice: DF.Link | None + contact_display: DF.SmallText | None + contact_email: DF.Data | None + contact_mobile: DF.Data | None + contact_person: DF.Link | None + conversion_rate: DF.Float + cost_center: DF.Link | None + coupon_code: DF.Link | None + currency: DF.Link + customer: DF.Link | None + customer_address: DF.Link | None + customer_group: DF.Link | None + customer_name: DF.Data | None + debit_to: DF.Link + discount_amount: DF.Currency + due_date: DF.Date | None + from_date: DF.Date | None + grand_total: DF.Currency + group_same_items: DF.Check + ignore_pricing_rule: DF.Check + in_words: DF.Data | None + inter_company_invoice_reference: DF.Link | None + is_discounted: DF.Check + is_opening: DF.Literal["No", "Yes"] + is_pos: DF.Check + is_return: DF.Check + items: DF.Table[POSInvoiceItem] + language: DF.Data | None + letter_head: DF.Link | None + loyalty_amount: DF.Currency + loyalty_points: DF.Int + loyalty_program: DF.Link | None + loyalty_redemption_account: DF.Link | None + loyalty_redemption_cost_center: DF.Link | None + naming_series: DF.Literal["ACC-PSINV-.YYYY.-"] + net_total: DF.Currency + other_charges_calculation: DF.TextEditor | None + outstanding_amount: DF.Currency + packed_items: DF.Table[PackedItem] + paid_amount: DF.Currency + party_account_currency: DF.Link | None + payment_schedule: DF.Table[PaymentSchedule] + payment_terms_template: DF.Link | None + payments: DF.Table[SalesInvoicePayment] + plc_conversion_rate: DF.Float + po_date: DF.Date | None + po_no: DF.Data | None + pos_profile: DF.Link | None + posting_date: DF.Date + posting_time: DF.Time | None + price_list_currency: DF.Link + pricing_rules: DF.Table[PricingRuleDetail] + project: DF.Link | None + redeem_loyalty_points: DF.Check + remarks: DF.SmallText | None + return_against: DF.Link | None + rounded_total: DF.Currency + rounding_adjustment: DF.Currency + sales_partner: DF.Link | None + sales_team: DF.Table[SalesTeam] + scan_barcode: DF.Data | None + select_print_heading: DF.Link | None + selling_price_list: DF.Link + set_posting_time: DF.Check + set_warehouse: DF.Link | None + shipping_address: DF.SmallText | None + shipping_address_name: DF.Link | None + shipping_rule: DF.Link | None + source: DF.Link | None + status: DF.Literal[ + "", + "Draft", + "Return", + "Credit Note Issued", + "Consolidated", + "Submitted", + "Paid", + "Unpaid", + "Unpaid and Discounted", + "Overdue and Discounted", + "Overdue", + "Cancelled", + ] + tax_category: DF.Link | None + tax_id: DF.Data | None + taxes: DF.Table[SalesTaxesandCharges] + taxes_and_charges: DF.Link | None + tc_name: DF.Link | None + terms: DF.TextEditor | None + territory: DF.Link | None + timesheets: DF.Table[SalesInvoiceTimesheet] + title: DF.Data | None + to_date: DF.Date | None + total: DF.Currency + total_advance: DF.Currency + total_billing_amount: DF.Currency + total_commission: DF.Currency + total_net_weight: DF.Float + total_qty: DF.Float + total_taxes_and_charges: DF.Currency + update_billed_amount_in_delivery_note: DF.Check + update_billed_amount_in_sales_order: DF.Check + update_stock: DF.Check + write_off_account: DF.Link | None + write_off_amount: DF.Currency + write_off_cost_center: DF.Link | None + write_off_outstanding_amount_automatically: DF.Check + # end: auto-generated types + +>>>>>>> 1c63983873 (fix: use Text Editor for rendering tax breakup table) def __init__(self, *args, **kwargs): super(POSInvoice, self).__init__(*args, **kwargs) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 88dd0113192e..6b0ec8e8c85d 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -756,7 +756,7 @@ }, { "fieldname": "other_charges_calculation", - "fieldtype": "Long Text", + "fieldtype": "Text Editor", "label": "Taxes and Charges Calculation", "no_copy": 1, "oldfieldtype": "HTML", @@ -1619,7 +1619,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2024-03-11 14:46:30.298184", + "modified": "2024-03-20 15:57:00.736868", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index f54787de717e..beff6d95ca2e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -54,6 +54,181 @@ class WarehouseMissingError(frappe.ValidationError): class PurchaseInvoice(BuyingController): +<<<<<<< HEAD +======= + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from erpnext.accounts.doctype.advance_tax.advance_tax import AdvanceTax + from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule + from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail + from erpnext.accounts.doctype.purchase_invoice_advance.purchase_invoice_advance import ( + PurchaseInvoiceAdvance, + ) + from erpnext.accounts.doctype.purchase_invoice_item.purchase_invoice_item import ( + PurchaseInvoiceItem, + ) + from erpnext.accounts.doctype.purchase_taxes_and_charges.purchase_taxes_and_charges import ( + PurchaseTaxesandCharges, + ) + from erpnext.accounts.doctype.tax_withheld_vouchers.tax_withheld_vouchers import ( + TaxWithheldVouchers, + ) + from erpnext.buying.doctype.purchase_receipt_item_supplied.purchase_receipt_item_supplied import ( + PurchaseReceiptItemSupplied, + ) + + additional_discount_percentage: DF.Float + address_display: DF.SmallText | None + advance_tax: DF.Table[AdvanceTax] + advances: DF.Table[PurchaseInvoiceAdvance] + against_expense_account: DF.SmallText | None + allocate_advances_automatically: DF.Check + amended_from: DF.Link | None + apply_discount_on: DF.Literal["", "Grand Total", "Net Total"] + apply_tds: DF.Check + auto_repeat: DF.Link | None + base_discount_amount: DF.Currency + base_grand_total: DF.Currency + base_in_words: DF.Data | None + base_net_total: DF.Currency + base_paid_amount: DF.Currency + base_rounded_total: DF.Currency + base_rounding_adjustment: DF.Currency + base_tax_withholding_net_total: DF.Currency + base_taxes_and_charges_added: DF.Currency + base_taxes_and_charges_deducted: DF.Currency + base_total: DF.Currency + base_total_taxes_and_charges: DF.Currency + base_write_off_amount: DF.Currency + bill_date: DF.Date | None + bill_no: DF.Data | None + billing_address: DF.Link | None + billing_address_display: DF.SmallText | None + buying_price_list: DF.Link | None + cash_bank_account: DF.Link | None + clearance_date: DF.Date | None + company: DF.Link | None + contact_display: DF.SmallText | None + contact_email: DF.SmallText | None + contact_mobile: DF.SmallText | None + contact_person: DF.Link | None + conversion_rate: DF.Float + cost_center: DF.Link | None + credit_to: DF.Link + currency: DF.Link | None + disable_rounded_total: DF.Check + discount_amount: DF.Currency + due_date: DF.Date | None + from_date: DF.Date | None + grand_total: DF.Currency + group_same_items: DF.Check + hold_comment: DF.SmallText | None + ignore_default_payment_terms_template: DF.Check + ignore_pricing_rule: DF.Check + in_words: DF.Data | None + incoterm: DF.Link | None + inter_company_invoice_reference: DF.Link | None + is_internal_supplier: DF.Check + is_old_subcontracting_flow: DF.Check + is_opening: DF.Literal["No", "Yes"] + is_paid: DF.Check + is_return: DF.Check + is_subcontracted: DF.Check + items: DF.Table[PurchaseInvoiceItem] + language: DF.Data | None + letter_head: DF.Link | None + mode_of_payment: DF.Link | None + named_place: DF.Data | None + naming_series: DF.Literal["ACC-PINV-.YYYY.-", "ACC-PINV-RET-.YYYY.-"] + net_total: DF.Currency + on_hold: DF.Check + only_include_allocated_payments: DF.Check + other_charges_calculation: DF.TextEditor | None + outstanding_amount: DF.Currency + paid_amount: DF.Currency + party_account_currency: DF.Link | None + payment_schedule: DF.Table[PaymentSchedule] + payment_terms_template: DF.Link | None + per_received: DF.Percent + plc_conversion_rate: DF.Float + posting_date: DF.Date + posting_time: DF.Time | None + price_list_currency: DF.Link | None + pricing_rules: DF.Table[PricingRuleDetail] + project: DF.Link | None + rejected_warehouse: DF.Link | None + release_date: DF.Date | None + remarks: DF.SmallText | None + repost_required: DF.Check + represents_company: DF.Link | None + return_against: DF.Link | None + rounded_total: DF.Currency + rounding_adjustment: DF.Currency + scan_barcode: DF.Data | None + select_print_heading: DF.Link | None + set_from_warehouse: DF.Link | None + set_posting_time: DF.Check + set_warehouse: DF.Link | None + shipping_address: DF.Link | None + shipping_address_display: DF.SmallText | None + shipping_rule: DF.Link | None + status: DF.Literal[ + "", + "Draft", + "Return", + "Debit Note Issued", + "Submitted", + "Paid", + "Partly Paid", + "Unpaid", + "Overdue", + "Cancelled", + "Internal Transfer", + ] + subscription: DF.Link | None + supplied_items: DF.Table[PurchaseReceiptItemSupplied] + supplier: DF.Link + supplier_address: DF.Link | None + supplier_group: DF.Link | None + supplier_name: DF.Data | None + supplier_warehouse: DF.Link | None + tax_category: DF.Link | None + tax_id: DF.ReadOnly | None + tax_withheld_vouchers: DF.Table[TaxWithheldVouchers] + tax_withholding_category: DF.Link | None + tax_withholding_net_total: DF.Currency + taxes: DF.Table[PurchaseTaxesandCharges] + taxes_and_charges: DF.Link | None + taxes_and_charges_added: DF.Currency + taxes_and_charges_deducted: DF.Currency + tc_name: DF.Link | None + terms: DF.TextEditor | None + title: DF.Data | None + to_date: DF.Date | None + total: DF.Currency + total_advance: DF.Currency + total_net_weight: DF.Float + total_qty: DF.Float + total_taxes_and_charges: DF.Currency + unrealized_profit_loss_account: DF.Link | None + update_billed_amount_in_purchase_order: DF.Check + update_billed_amount_in_purchase_receipt: DF.Check + update_outstanding_for_self: DF.Check + update_stock: DF.Check + use_company_roundoff_cost_center: DF.Check + use_transaction_date_exchange_rate: DF.Check + write_off_account: DF.Link | None + write_off_amount: DF.Currency + write_off_cost_center: DF.Link | None + # end: auto-generated types + +>>>>>>> 1c63983873 (fix: use Text Editor for rendering tax breakup table) def __init__(self, *args, **kwargs): super(PurchaseInvoice, self).__init__(*args, **kwargs) self.status_updater = [ diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 865362fbb482..ddb82d95f9de 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -943,7 +943,7 @@ }, { "fieldname": "other_charges_calculation", - "fieldtype": "Long Text", + "fieldtype": "Text Editor", "hide_days": 1, "hide_seconds": 1, "label": "Taxes and Charges Calculation", @@ -2183,7 +2183,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2024-03-15 16:44:17.778370", + "modified": "2024-03-20 16:02:52.237732", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", @@ -2238,4 +2238,4 @@ "title_field": "title", "track_changes": 1, "track_seen": 1 -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 2a86d0daf45e..02f5259f4aa3 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -49,6 +49,194 @@ class SalesInvoice(SellingController): +<<<<<<< HEAD +======= + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule + from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail + from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import ( + SalesInvoiceAdvance, + ) + from erpnext.accounts.doctype.sales_invoice_item.sales_invoice_item import SalesInvoiceItem + from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import ( + SalesInvoicePayment, + ) + from erpnext.accounts.doctype.sales_invoice_timesheet.sales_invoice_timesheet import ( + SalesInvoiceTimesheet, + ) + from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import ( + SalesTaxesandCharges, + ) + from erpnext.selling.doctype.sales_team.sales_team import SalesTeam + from erpnext.stock.doctype.packed_item.packed_item import PackedItem + + account_for_change_amount: DF.Link | None + additional_discount_account: DF.Link | None + additional_discount_percentage: DF.Float + address_display: DF.SmallText | None + advances: DF.Table[SalesInvoiceAdvance] + against_income_account: DF.SmallText | None + allocate_advances_automatically: DF.Check + amended_from: DF.Link | None + amount_eligible_for_commission: DF.Currency + apply_discount_on: DF.Literal["", "Grand Total", "Net Total"] + auto_repeat: DF.Link | None + base_change_amount: DF.Currency + base_discount_amount: DF.Currency + base_grand_total: DF.Currency + base_in_words: DF.SmallText | None + base_net_total: DF.Currency + base_paid_amount: DF.Currency + base_rounded_total: DF.Currency + base_rounding_adjustment: DF.Currency + base_total: DF.Currency + base_total_taxes_and_charges: DF.Currency + base_write_off_amount: DF.Currency + campaign: DF.Link | None + cash_bank_account: DF.Link | None + change_amount: DF.Currency + commission_rate: DF.Float + company: DF.Link + company_address: DF.Link | None + company_address_display: DF.SmallText | None + company_tax_id: DF.Data | None + contact_display: DF.SmallText | None + contact_email: DF.Data | None + contact_mobile: DF.SmallText | None + contact_person: DF.Link | None + conversion_rate: DF.Float + cost_center: DF.Link | None + currency: DF.Link + customer: DF.Link | None + customer_address: DF.Link | None + customer_group: DF.Link | None + customer_name: DF.SmallText | None + debit_to: DF.Link + disable_rounded_total: DF.Check + discount_amount: DF.Currency + dispatch_address: DF.SmallText | None + dispatch_address_name: DF.Link | None + dont_create_loyalty_points: DF.Check + due_date: DF.Date | None + from_date: DF.Date | None + grand_total: DF.Currency + group_same_items: DF.Check + ignore_default_payment_terms_template: DF.Check + ignore_pricing_rule: DF.Check + in_words: DF.SmallText | None + incoterm: DF.Link | None + inter_company_invoice_reference: DF.Link | None + is_cash_or_non_trade_discount: DF.Check + is_consolidated: DF.Check + is_debit_note: DF.Check + is_discounted: DF.Check + is_internal_customer: DF.Check + is_opening: DF.Literal["No", "Yes"] + is_pos: DF.Check + is_return: DF.Check + items: DF.Table[SalesInvoiceItem] + language: DF.Data | None + letter_head: DF.Link | None + loyalty_amount: DF.Currency + loyalty_points: DF.Int + loyalty_program: DF.Link | None + loyalty_redemption_account: DF.Link | None + loyalty_redemption_cost_center: DF.Link | None + named_place: DF.Data | None + naming_series: DF.Literal["ACC-SINV-.YYYY.-", "ACC-SINV-RET-.YYYY.-"] + net_total: DF.Currency + only_include_allocated_payments: DF.Check + other_charges_calculation: DF.TextEditor | None + outstanding_amount: DF.Currency + packed_items: DF.Table[PackedItem] + paid_amount: DF.Currency + party_account_currency: DF.Link | None + payment_schedule: DF.Table[PaymentSchedule] + payment_terms_template: DF.Link | None + payments: DF.Table[SalesInvoicePayment] + plc_conversion_rate: DF.Float + po_date: DF.Date | None + po_no: DF.Data | None + pos_profile: DF.Link | None + posting_date: DF.Date + posting_time: DF.Time | None + price_list_currency: DF.Link + pricing_rules: DF.Table[PricingRuleDetail] + project: DF.Link | None + redeem_loyalty_points: DF.Check + remarks: DF.SmallText | None + repost_required: DF.Check + represents_company: DF.Link | None + return_against: DF.Link | None + rounded_total: DF.Currency + rounding_adjustment: DF.Currency + sales_partner: DF.Link | None + sales_team: DF.Table[SalesTeam] + scan_barcode: DF.Data | None + select_print_heading: DF.Link | None + selling_price_list: DF.Link + set_posting_time: DF.Check + set_target_warehouse: DF.Link | None + set_warehouse: DF.Link | None + shipping_address: DF.SmallText | None + shipping_address_name: DF.Link | None + shipping_rule: DF.Link | None + source: DF.Link | None + status: DF.Literal[ + "", + "Draft", + "Return", + "Credit Note Issued", + "Submitted", + "Paid", + "Partly Paid", + "Unpaid", + "Unpaid and Discounted", + "Partly Paid and Discounted", + "Overdue and Discounted", + "Overdue", + "Cancelled", + "Internal Transfer", + ] + subscription: DF.Link | None + tax_category: DF.Link | None + tax_id: DF.Data | None + taxes: DF.Table[SalesTaxesandCharges] + taxes_and_charges: DF.Link | None + tc_name: DF.Link | None + terms: DF.TextEditor | None + territory: DF.Link | None + timesheets: DF.Table[SalesInvoiceTimesheet] + title: DF.Data | None + to_date: DF.Date | None + total: DF.Currency + total_advance: DF.Currency + total_billing_amount: DF.Currency + total_billing_hours: DF.Float + total_commission: DF.Currency + total_net_weight: DF.Float + total_qty: DF.Float + total_taxes_and_charges: DF.Currency + unrealized_profit_loss_account: DF.Link | None + update_billed_amount_in_delivery_note: DF.Check + update_billed_amount_in_sales_order: DF.Check + update_outstanding_for_self: DF.Check + update_stock: DF.Check + use_company_roundoff_cost_center: DF.Check + write_off_account: DF.Link | None + write_off_amount: DF.Currency + write_off_cost_center: DF.Link | None + write_off_outstanding_amount_automatically: DF.Check + # end: auto-generated types + +>>>>>>> 1c63983873 (fix: use Text Editor for rendering tax breakup table) def __init__(self, *args, **kwargs): super(SalesInvoice, self).__init__(*args, **kwargs) self.status_updater = [ diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 01ce8c33ff9d..a2bb42bfe7d7 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -639,7 +639,7 @@ }, { "fieldname": "other_charges_calculation", - "fieldtype": "Long Text", + "fieldtype": "Text Editor", "label": "Taxes and Charges Calculation", "no_copy": 1, "oldfieldtype": "HTML", @@ -1273,7 +1273,11 @@ "idx": 105, "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2023-10-01 20:58:07.851037", +======= + "modified": "2024-03-20 16:03:31.611808", +>>>>>>> 1c63983873 (fix: use Text Editor for rendering tax breakup table) "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 9d4846056eaf..cca058f806aa 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -33,6 +33,141 @@ class PurchaseOrder(BuyingController): +<<<<<<< HEAD +======= + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule + from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail + from erpnext.accounts.doctype.purchase_taxes_and_charges.purchase_taxes_and_charges import ( + PurchaseTaxesandCharges, + ) + from erpnext.buying.doctype.purchase_order_item.purchase_order_item import PurchaseOrderItem + from erpnext.buying.doctype.purchase_order_item_supplied.purchase_order_item_supplied import ( + PurchaseOrderItemSupplied, + ) + + additional_discount_percentage: DF.Float + address_display: DF.SmallText | None + advance_paid: DF.Currency + advance_payment_status: DF.Literal["Not Initiated", "Initiated", "Partially Paid", "Fully Paid"] + amended_from: DF.Link | None + apply_discount_on: DF.Literal["", "Grand Total", "Net Total"] + apply_tds: DF.Check + auto_repeat: DF.Link | None + base_discount_amount: DF.Currency + base_grand_total: DF.Currency + base_in_words: DF.Data | None + base_net_total: DF.Currency + base_rounded_total: DF.Currency + base_rounding_adjustment: DF.Currency + base_tax_withholding_net_total: DF.Currency + base_taxes_and_charges_added: DF.Currency + base_taxes_and_charges_deducted: DF.Currency + base_total: DF.Currency + base_total_taxes_and_charges: DF.Currency + billing_address: DF.Link | None + billing_address_display: DF.SmallText | None + buying_price_list: DF.Link | None + company: DF.Link + contact_display: DF.SmallText | None + contact_email: DF.SmallText | None + contact_mobile: DF.SmallText | None + contact_person: DF.Link | None + conversion_rate: DF.Float + cost_center: DF.Link | None + currency: DF.Link + customer: DF.Link | None + customer_contact_display: DF.SmallText | None + customer_contact_email: DF.Code | None + customer_contact_mobile: DF.SmallText | None + customer_contact_person: DF.Link | None + customer_name: DF.Data | None + disable_rounded_total: DF.Check + discount_amount: DF.Currency + from_date: DF.Date | None + grand_total: DF.Currency + group_same_items: DF.Check + ignore_pricing_rule: DF.Check + in_words: DF.Data | None + incoterm: DF.Link | None + inter_company_order_reference: DF.Link | None + is_internal_supplier: DF.Check + is_old_subcontracting_flow: DF.Check + is_subcontracted: DF.Check + items: DF.Table[PurchaseOrderItem] + language: DF.Data | None + letter_head: DF.Link | None + named_place: DF.Data | None + naming_series: DF.Literal["PUR-ORD-.YYYY.-"] + net_total: DF.Currency + order_confirmation_date: DF.Date | None + order_confirmation_no: DF.Data | None + other_charges_calculation: DF.TextEditor | None + party_account_currency: DF.Link | None + payment_schedule: DF.Table[PaymentSchedule] + payment_terms_template: DF.Link | None + per_billed: DF.Percent + per_received: DF.Percent + plc_conversion_rate: DF.Float + price_list_currency: DF.Link | None + pricing_rules: DF.Table[PricingRuleDetail] + project: DF.Link | None + ref_sq: DF.Link | None + represents_company: DF.Link | None + rounded_total: DF.Currency + rounding_adjustment: DF.Currency + scan_barcode: DF.Data | None + schedule_date: DF.Date | None + select_print_heading: DF.Link | None + set_from_warehouse: DF.Link | None + set_reserve_warehouse: DF.Link | None + set_warehouse: DF.Link | None + shipping_address: DF.Link | None + shipping_address_display: DF.SmallText | None + shipping_rule: DF.Link | None + status: DF.Literal[ + "", + "Draft", + "On Hold", + "To Receive and Bill", + "To Bill", + "To Receive", + "Completed", + "Cancelled", + "Closed", + "Delivered", + ] + supplied_items: DF.Table[PurchaseOrderItemSupplied] + supplier: DF.Link + supplier_address: DF.Link | None + supplier_name: DF.Data | None + supplier_warehouse: DF.Link | None + tax_category: DF.Link | None + tax_withholding_category: DF.Link | None + tax_withholding_net_total: DF.Currency + taxes: DF.Table[PurchaseTaxesandCharges] + taxes_and_charges: DF.Link | None + taxes_and_charges_added: DF.Currency + taxes_and_charges_deducted: DF.Currency + tc_name: DF.Link | None + terms: DF.TextEditor | None + title: DF.Data + to_date: DF.Date | None + total: DF.Currency + total_net_weight: DF.Float + total_qty: DF.Float + total_taxes_and_charges: DF.Currency + transaction_date: DF.Date + # end: auto-generated types + +>>>>>>> 1c63983873 (fix: use Text Editor for rendering tax breakup table) def __init__(self, *args, **kwargs): super(PurchaseOrder, self).__init__(*args, **kwargs) self.status_updater = [ diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json index aa89b81b5ed9..7716a15ccbab 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json @@ -461,7 +461,7 @@ }, { "fieldname": "other_charges_calculation", - "fieldtype": "Long Text", + "fieldtype": "Markdown Editor", "label": "Taxes and Charges Calculation", "no_copy": 1, "oldfieldtype": "HTML", @@ -927,7 +927,11 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2023-11-20 11:15:30.083077", +======= + "modified": "2024-03-20 16:03:59.069145", +>>>>>>> 1c63983873 (fix: use Text Editor for rendering tax breakup table) "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation", diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py index e27fbe8aaa23..70fea9135917 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py @@ -14,6 +14,98 @@ class SupplierQuotation(BuyingController): +<<<<<<< HEAD +======= + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail + from erpnext.accounts.doctype.purchase_taxes_and_charges.purchase_taxes_and_charges import ( + PurchaseTaxesandCharges, + ) + from erpnext.buying.doctype.supplier_quotation_item.supplier_quotation_item import ( + SupplierQuotationItem, + ) + + additional_discount_percentage: DF.Float + address_display: DF.SmallText | None + amended_from: DF.Link | None + apply_discount_on: DF.Literal["", "Grand Total", "Net Total"] + auto_repeat: DF.Link | None + base_discount_amount: DF.Currency + base_grand_total: DF.Currency + base_in_words: DF.Data | None + base_net_total: DF.Currency + base_rounded_total: DF.Currency + base_rounding_adjustment: DF.Currency + base_taxes_and_charges_added: DF.Currency + base_taxes_and_charges_deducted: DF.Currency + base_total: DF.Currency + base_total_taxes_and_charges: DF.Currency + billing_address: DF.Link | None + billing_address_display: DF.SmallText | None + buying_price_list: DF.Link | None + company: DF.Link + contact_display: DF.SmallText | None + contact_email: DF.Data | None + contact_mobile: DF.SmallText | None + contact_person: DF.Link | None + conversion_rate: DF.Float + cost_center: DF.Link | None + currency: DF.Link + disable_rounded_total: DF.Check + discount_amount: DF.Currency + grand_total: DF.Currency + group_same_items: DF.Check + ignore_pricing_rule: DF.Check + in_words: DF.Data | None + incoterm: DF.Link | None + is_subcontracted: DF.Check + items: DF.Table[SupplierQuotationItem] + language: DF.Data | None + letter_head: DF.Link | None + named_place: DF.Data | None + naming_series: DF.Literal["PUR-SQTN-.YYYY.-"] + net_total: DF.Currency + opportunity: DF.Link | None + other_charges_calculation: DF.MarkdownEditor | None + plc_conversion_rate: DF.Float + price_list_currency: DF.Link | None + pricing_rules: DF.Table[PricingRuleDetail] + project: DF.Link | None + quotation_number: DF.Data | None + rounded_total: DF.Currency + rounding_adjustment: DF.Currency + select_print_heading: DF.Link | None + shipping_address: DF.Link | None + shipping_address_display: DF.SmallText | None + shipping_rule: DF.Link | None + status: DF.Literal["", "Draft", "Submitted", "Stopped", "Cancelled", "Expired"] + supplier: DF.Link + supplier_address: DF.Link | None + supplier_name: DF.Data | None + tax_category: DF.Link | None + taxes: DF.Table[PurchaseTaxesandCharges] + taxes_and_charges: DF.Link | None + taxes_and_charges_added: DF.Currency + taxes_and_charges_deducted: DF.Currency + tc_name: DF.Link | None + terms: DF.TextEditor | None + title: DF.Data | None + total: DF.Currency + total_net_weight: DF.Float + total_qty: DF.Float + total_taxes_and_charges: DF.Currency + transaction_date: DF.Date + valid_till: DF.Date | None + # end: auto-generated types + +>>>>>>> 1c63983873 (fix: use Text Editor for rendering tax breakup table) def validate(self): super(SupplierQuotation, self).validate() diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index 2ffa6a5c120f..f3c5439d9879 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -556,7 +556,7 @@ }, { "fieldname": "other_charges_calculation", - "fieldtype": "Long Text", + "fieldtype": "Text Editor", "label": "Taxes and Charges Calculation", "no_copy": 1, "oldfieldtype": "HTML", @@ -1072,7 +1072,11 @@ "idx": 82, "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2023-04-14 16:50:44.550098", +======= + "modified": "2024-03-20 16:04:21.567847", +>>>>>>> 1c63983873 (fix: use Text Editor for rendering tax breakup table) "modified_by": "Administrator", "module": "Selling", "name": "Quotation", diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index de2cef909299..d09b1d30838a 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -13,6 +13,112 @@ class Quotation(SellingController): +<<<<<<< HEAD +======= + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule + from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail + from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import ( + SalesTaxesandCharges, + ) + from erpnext.crm.doctype.competitor_detail.competitor_detail import CompetitorDetail + from erpnext.selling.doctype.quotation_item.quotation_item import QuotationItem + from erpnext.setup.doctype.quotation_lost_reason_detail.quotation_lost_reason_detail import ( + QuotationLostReasonDetail, + ) + from erpnext.stock.doctype.packed_item.packed_item import PackedItem + + additional_discount_percentage: DF.Float + address_display: DF.SmallText | None + amended_from: DF.Link | None + apply_discount_on: DF.Literal["", "Grand Total", "Net Total"] + auto_repeat: DF.Link | None + base_discount_amount: DF.Currency + base_grand_total: DF.Currency + base_in_words: DF.Data | None + base_net_total: DF.Currency + base_rounded_total: DF.Currency + base_rounding_adjustment: DF.Currency + base_total: DF.Currency + base_total_taxes_and_charges: DF.Currency + campaign: DF.Link | None + company: DF.Link + company_address: DF.Link | None + company_address_display: DF.SmallText | None + competitors: DF.TableMultiSelect[CompetitorDetail] + contact_display: DF.SmallText | None + contact_email: DF.Data | None + contact_mobile: DF.SmallText | None + contact_person: DF.Link | None + conversion_rate: DF.Float + coupon_code: DF.Link | None + currency: DF.Link + customer_address: DF.Link | None + customer_group: DF.Link | None + customer_name: DF.Data | None + discount_amount: DF.Currency + enq_det: DF.Text | None + grand_total: DF.Currency + group_same_items: DF.Check + ignore_pricing_rule: DF.Check + in_words: DF.Data | None + incoterm: DF.Link | None + items: DF.Table[QuotationItem] + language: DF.Data | None + letter_head: DF.Link | None + lost_reasons: DF.TableMultiSelect[QuotationLostReasonDetail] + named_place: DF.Data | None + naming_series: DF.Literal["SAL-QTN-.YYYY.-"] + net_total: DF.Currency + opportunity: DF.Link | None + order_lost_reason: DF.SmallText | None + order_type: DF.Literal["", "Sales", "Maintenance", "Shopping Cart"] + other_charges_calculation: DF.TextEditor | None + packed_items: DF.Table[PackedItem] + party_name: DF.DynamicLink | None + payment_schedule: DF.Table[PaymentSchedule] + payment_terms_template: DF.Link | None + plc_conversion_rate: DF.Float + price_list_currency: DF.Link + pricing_rules: DF.Table[PricingRuleDetail] + quotation_to: DF.Link + referral_sales_partner: DF.Link | None + rounded_total: DF.Currency + rounding_adjustment: DF.Currency + scan_barcode: DF.Data | None + select_print_heading: DF.Link | None + selling_price_list: DF.Link + shipping_address: DF.SmallText | None + shipping_address_name: DF.Link | None + shipping_rule: DF.Link | None + source: DF.Link | None + status: DF.Literal[ + "Draft", "Open", "Replied", "Partially Ordered", "Ordered", "Lost", "Cancelled", "Expired" + ] + supplier_quotation: DF.Link | None + tax_category: DF.Link | None + taxes: DF.Table[SalesTaxesandCharges] + taxes_and_charges: DF.Link | None + tc_name: DF.Link | None + terms: DF.TextEditor | None + territory: DF.Link | None + title: DF.Data | None + total: DF.Currency + total_net_weight: DF.Float + total_qty: DF.Float + total_taxes_and_charges: DF.Currency + transaction_date: DF.Date + valid_till: DF.Date | None + # end: auto-generated types + +>>>>>>> 1c63983873 (fix: use Text Editor for rendering tax breakup table) def set_indicator(self): if self.docstatus == 1: self.indicator_color = "blue" diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 490bd7a98308..09b73878aa21 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -774,7 +774,7 @@ }, { "fieldname": "other_charges_calculation", - "fieldtype": "Long Text", + "fieldtype": "Text Editor", "hide_days": 1, "hide_seconds": 1, "label": "Taxes and Charges Calculation", @@ -1631,7 +1631,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2023-10-18 12:41:54.813462", + "modified": "2024-03-20 16:04:43.627183", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 81f9cedbc626..da196c7ac4e0 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -41,6 +41,144 @@ class WarehouseRequired(frappe.ValidationError): class SalesOrder(SellingController): +<<<<<<< HEAD +======= + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule + from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail + from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import ( + SalesTaxesandCharges, + ) + from erpnext.selling.doctype.sales_order_item.sales_order_item import SalesOrderItem + from erpnext.selling.doctype.sales_team.sales_team import SalesTeam + from erpnext.stock.doctype.packed_item.packed_item import PackedItem + + additional_discount_percentage: DF.Float + address_display: DF.SmallText | None + advance_paid: DF.Currency + advance_payment_status: DF.Literal["Not Requested", "Requested", "Partially Paid", "Fully Paid"] + amended_from: DF.Link | None + amount_eligible_for_commission: DF.Currency + apply_discount_on: DF.Literal["", "Grand Total", "Net Total"] + auto_repeat: DF.Link | None + base_discount_amount: DF.Currency + base_grand_total: DF.Currency + base_in_words: DF.Data | None + base_net_total: DF.Currency + base_rounded_total: DF.Currency + base_rounding_adjustment: DF.Currency + base_total: DF.Currency + base_total_taxes_and_charges: DF.Currency + billing_status: DF.Literal["Not Billed", "Fully Billed", "Partly Billed", "Closed"] + campaign: DF.Link | None + commission_rate: DF.Float + company: DF.Link + company_address: DF.Link | None + company_address_display: DF.SmallText | None + contact_display: DF.SmallText | None + contact_email: DF.Data | None + contact_mobile: DF.SmallText | None + contact_person: DF.Link | None + contact_phone: DF.Data | None + conversion_rate: DF.Float + cost_center: DF.Link | None + coupon_code: DF.Link | None + currency: DF.Link + customer: DF.Link + customer_address: DF.Link | None + customer_group: DF.Link | None + customer_name: DF.Data | None + delivery_date: DF.Date | None + delivery_status: DF.Literal[ + "Not Delivered", "Fully Delivered", "Partly Delivered", "Closed", "Not Applicable" + ] + disable_rounded_total: DF.Check + discount_amount: DF.Currency + dispatch_address: DF.SmallText | None + dispatch_address_name: DF.Link | None + from_date: DF.Date | None + grand_total: DF.Currency + group_same_items: DF.Check + ignore_pricing_rule: DF.Check + in_words: DF.Data | None + incoterm: DF.Link | None + inter_company_order_reference: DF.Link | None + is_internal_customer: DF.Check + items: DF.Table[SalesOrderItem] + language: DF.Data | None + letter_head: DF.Link | None + loyalty_amount: DF.Currency + loyalty_points: DF.Int + named_place: DF.Data | None + naming_series: DF.Literal["SAL-ORD-.YYYY.-"] + net_total: DF.Currency + order_type: DF.Literal["", "Sales", "Maintenance", "Shopping Cart"] + other_charges_calculation: DF.TextEditor | None + packed_items: DF.Table[PackedItem] + party_account_currency: DF.Link | None + payment_schedule: DF.Table[PaymentSchedule] + payment_terms_template: DF.Link | None + per_billed: DF.Percent + per_delivered: DF.Percent + per_picked: DF.Percent + plc_conversion_rate: DF.Float + po_date: DF.Date | None + po_no: DF.Data | None + price_list_currency: DF.Link + pricing_rules: DF.Table[PricingRuleDetail] + project: DF.Link | None + represents_company: DF.Link | None + reserve_stock: DF.Check + rounded_total: DF.Currency + rounding_adjustment: DF.Currency + sales_partner: DF.Link | None + sales_team: DF.Table[SalesTeam] + scan_barcode: DF.Data | None + select_print_heading: DF.Link | None + selling_price_list: DF.Link + set_warehouse: DF.Link | None + shipping_address: DF.SmallText | None + shipping_address_name: DF.Link | None + shipping_rule: DF.Link | None + skip_delivery_note: DF.Check + source: DF.Link | None + status: DF.Literal[ + "", + "Draft", + "On Hold", + "To Pay", + "To Deliver and Bill", + "To Bill", + "To Deliver", + "Completed", + "Cancelled", + "Closed", + ] + tax_category: DF.Link | None + tax_id: DF.Data | None + taxes: DF.Table[SalesTaxesandCharges] + taxes_and_charges: DF.Link | None + tc_name: DF.Link | None + terms: DF.TextEditor | None + territory: DF.Link | None + title: DF.Data | None + to_date: DF.Date | None + total: DF.Currency + total_commission: DF.Currency + total_net_weight: DF.Float + total_qty: DF.Float + total_taxes_and_charges: DF.Currency + transaction_date: DF.Date + # end: auto-generated types + +>>>>>>> 1c63983873 (fix: use Text Editor for rendering tax breakup table) def __init__(self, *args, **kwargs): super(SalesOrder, self).__init__(*args, **kwargs) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 5731bda495ee..ec22e5501453 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -680,7 +680,7 @@ }, { "fieldname": "other_charges_calculation", - "fieldtype": "Long Text", + "fieldtype": "Text Editor", "label": "Taxes and Charges Calculation", "no_copy": 1, "oldfieldtype": "HTML", @@ -1401,7 +1401,11 @@ "idx": 146, "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2023-12-18 17:19:39.368239", +======= + "modified": "2024-03-20 16:05:02.854990", +>>>>>>> 1c63983873 (fix: use Text Editor for rendering tax breakup table) "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index a402bb5aed28..86e07bcd2624 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -19,6 +19,131 @@ class DeliveryNote(SellingController): +<<<<<<< HEAD +======= + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail + from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import ( + SalesTaxesandCharges, + ) + from erpnext.selling.doctype.sales_team.sales_team import SalesTeam + from erpnext.stock.doctype.delivery_note_item.delivery_note_item import DeliveryNoteItem + from erpnext.stock.doctype.packed_item.packed_item import PackedItem + + additional_discount_percentage: DF.Float + address_display: DF.SmallText | None + amended_from: DF.Link | None + amount_eligible_for_commission: DF.Currency + apply_discount_on: DF.Literal["", "Grand Total", "Net Total"] + auto_repeat: DF.Link | None + base_discount_amount: DF.Currency + base_grand_total: DF.Currency + base_in_words: DF.Data | None + base_net_total: DF.Currency + base_rounded_total: DF.Currency + base_rounding_adjustment: DF.Currency + base_total: DF.Currency + base_total_taxes_and_charges: DF.Currency + campaign: DF.Link | None + commission_rate: DF.Float + company: DF.Link + company_address: DF.Link | None + company_address_display: DF.SmallText | None + contact_display: DF.SmallText | None + contact_email: DF.Data | None + contact_mobile: DF.SmallText | None + contact_person: DF.Link | None + conversion_rate: DF.Float + cost_center: DF.Link | None + currency: DF.Link + customer: DF.Link + customer_address: DF.Link | None + customer_group: DF.Link | None + customer_name: DF.Data | None + disable_rounded_total: DF.Check + discount_amount: DF.Currency + dispatch_address: DF.SmallText | None + dispatch_address_name: DF.Link | None + driver: DF.Link | None + driver_name: DF.Data | None + excise_page: DF.Data | None + grand_total: DF.Currency + group_same_items: DF.Check + ignore_pricing_rule: DF.Check + in_words: DF.Data | None + incoterm: DF.Link | None + installation_status: DF.Literal[None] + instructions: DF.Text | None + inter_company_reference: DF.Link | None + is_internal_customer: DF.Check + is_return: DF.Check + issue_credit_note: DF.Check + items: DF.Table[DeliveryNoteItem] + language: DF.Data | None + letter_head: DF.Link | None + lr_date: DF.Date | None + lr_no: DF.Data | None + named_place: DF.Data | None + naming_series: DF.Literal["MAT-DN-.YYYY.-", "MAT-DN-RET-.YYYY.-"] + net_total: DF.Currency + other_charges_calculation: DF.TextEditor | None + packed_items: DF.Table[PackedItem] + per_billed: DF.Percent + per_installed: DF.Percent + per_returned: DF.Percent + pick_list: DF.Link | None + plc_conversion_rate: DF.Float + po_date: DF.Date | None + po_no: DF.SmallText | None + posting_date: DF.Date + posting_time: DF.Time + price_list_currency: DF.Link + pricing_rules: DF.Table[PricingRuleDetail] + print_without_amount: DF.Check + project: DF.Link | None + represents_company: DF.Link | None + return_against: DF.Link | None + rounded_total: DF.Currency + rounding_adjustment: DF.Currency + sales_partner: DF.Link | None + sales_team: DF.Table[SalesTeam] + scan_barcode: DF.Data | None + select_print_heading: DF.Link | None + selling_price_list: DF.Link + set_posting_time: DF.Check + set_target_warehouse: DF.Link | None + set_warehouse: DF.Link | None + shipping_address: DF.SmallText | None + shipping_address_name: DF.Link | None + shipping_rule: DF.Link | None + source: DF.Link | None + status: DF.Literal["", "Draft", "To Bill", "Completed", "Return Issued", "Cancelled", "Closed"] + tax_category: DF.Link | None + tax_id: DF.Data | None + taxes: DF.Table[SalesTaxesandCharges] + taxes_and_charges: DF.Link | None + tc_name: DF.Link | None + terms: DF.TextEditor | None + territory: DF.Link | None + title: DF.Data | None + total: DF.Currency + total_commission: DF.Currency + total_net_weight: DF.Float + total_qty: DF.Float + total_taxes_and_charges: DF.Currency + transporter: DF.Link | None + transporter_name: DF.Data | None + vehicle_no: DF.Data | None + # end: auto-generated types + +>>>>>>> 1c63983873 (fix: use Text Editor for rendering tax breakup table) def __init__(self, *args, **kwargs): super(DeliveryNote, self).__init__(*args, **kwargs) self.status_updater = [ diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index de1263d8f66f..28c54dab9b25 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -649,7 +649,7 @@ }, { "fieldname": "other_charges_calculation", - "fieldtype": "Long Text", + "fieldtype": "Text Editor", "label": "Taxes and Charges Calculation", "no_copy": 1, "oldfieldtype": "HTML", @@ -1242,7 +1242,7 @@ "idx": 261, "is_submittable": 1, "links": [], - "modified": "2023-12-18 17:26:41.279663", + "modified": "2024-03-20 16:05:31.713453", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 79e6ab84d950..e58fca996439 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -21,6 +21,121 @@ class PurchaseReceipt(BuyingController): +<<<<<<< HEAD +======= + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail + from erpnext.accounts.doctype.purchase_taxes_and_charges.purchase_taxes_and_charges import ( + PurchaseTaxesandCharges, + ) + from erpnext.buying.doctype.purchase_receipt_item_supplied.purchase_receipt_item_supplied import ( + PurchaseReceiptItemSupplied, + ) + from erpnext.stock.doctype.purchase_receipt_item.purchase_receipt_item import PurchaseReceiptItem + + additional_discount_percentage: DF.Float + address_display: DF.SmallText | None + amended_from: DF.Link | None + apply_discount_on: DF.Literal["", "Grand Total", "Net Total"] + apply_putaway_rule: DF.Check + auto_repeat: DF.Link | None + base_discount_amount: DF.Currency + base_grand_total: DF.Currency + base_in_words: DF.Data | None + base_net_total: DF.Currency + base_rounded_total: DF.Currency + base_rounding_adjustment: DF.Currency + base_taxes_and_charges_added: DF.Currency + base_taxes_and_charges_deducted: DF.Currency + base_total: DF.Currency + base_total_taxes_and_charges: DF.Currency + billing_address: DF.Link | None + billing_address_display: DF.SmallText | None + buying_price_list: DF.Link | None + company: DF.Link + contact_display: DF.SmallText | None + contact_email: DF.SmallText | None + contact_mobile: DF.SmallText | None + contact_person: DF.Link | None + conversion_rate: DF.Float + cost_center: DF.Link | None + currency: DF.Link + disable_rounded_total: DF.Check + discount_amount: DF.Currency + grand_total: DF.Currency + group_same_items: DF.Check + ignore_pricing_rule: DF.Check + in_words: DF.Data | None + incoterm: DF.Link | None + instructions: DF.SmallText | None + inter_company_reference: DF.Link | None + is_internal_supplier: DF.Check + is_old_subcontracting_flow: DF.Check + is_return: DF.Check + is_subcontracted: DF.Check + items: DF.Table[PurchaseReceiptItem] + language: DF.Data | None + letter_head: DF.Link | None + lr_date: DF.Date | None + lr_no: DF.Data | None + named_place: DF.Data | None + naming_series: DF.Literal["MAT-PRE-.YYYY.-", "MAT-PR-RET-.YYYY.-"] + net_total: DF.Currency + other_charges_calculation: DF.TextEditor | None + per_billed: DF.Percent + per_returned: DF.Percent + plc_conversion_rate: DF.Float + posting_date: DF.Date + posting_time: DF.Time + price_list_currency: DF.Link | None + pricing_rules: DF.Table[PricingRuleDetail] + project: DF.Link | None + range: DF.Data | None + rejected_warehouse: DF.Link | None + remarks: DF.SmallText | None + represents_company: DF.Link | None + return_against: DF.Link | None + rounded_total: DF.Currency + rounding_adjustment: DF.Currency + scan_barcode: DF.Data | None + select_print_heading: DF.Link | None + set_from_warehouse: DF.Link | None + set_posting_time: DF.Check + set_warehouse: DF.Link | None + shipping_address: DF.Link | None + shipping_address_display: DF.SmallText | None + shipping_rule: DF.Link | None + status: DF.Literal["", "Draft", "To Bill", "Completed", "Return Issued", "Cancelled", "Closed"] + subcontracting_receipt: DF.Link | None + supplied_items: DF.Table[PurchaseReceiptItemSupplied] + supplier: DF.Link + supplier_address: DF.Link | None + supplier_delivery_note: DF.Data | None + supplier_name: DF.Data | None + supplier_warehouse: DF.Link | None + tax_category: DF.Link | None + taxes: DF.Table[PurchaseTaxesandCharges] + taxes_and_charges: DF.Link | None + taxes_and_charges_added: DF.Currency + taxes_and_charges_deducted: DF.Currency + tc_name: DF.Link | None + terms: DF.TextEditor | None + title: DF.Data | None + total: DF.Currency + total_net_weight: DF.Float + total_qty: DF.Float + total_taxes_and_charges: DF.Currency + transporter_name: DF.Data | None + # end: auto-generated types + +>>>>>>> 1c63983873 (fix: use Text Editor for rendering tax breakup table) def __init__(self, *args, **kwargs): super(PurchaseReceipt, self).__init__(*args, **kwargs) self.status_updater = [ From e922ac7c31a9f3d7ec5b43a1601638266da2e81e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 20 Mar 2024 16:56:30 +0530 Subject: [PATCH 06/94] fix: style for tax breakup (cherry picked from commit 967540da18351705dfdcf7bebae62d5445eec7e2) # Conflicts: # erpnext/public/scss/erpnext.scss --- erpnext/public/scss/erpnext.scss | 59 ++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/erpnext/public/scss/erpnext.scss b/erpnext/public/scss/erpnext.scss index 6da8f24cf9b6..9d65166f8a7b 100644 --- a/erpnext/public/scss/erpnext.scss +++ b/erpnext/public/scss/erpnext.scss @@ -496,3 +496,62 @@ body[data-route="pos"] { .exercise-col { padding: 10px; } +<<<<<<< HEAD +======= + +.plant-floor, +.workstation-wrapper, +.workstation-card p { + border-radius: var(--border-radius-md); + border: 1px solid var(--border-color); + box-shadow: none; + background-color: var(--card-bg); + position: relative; +} + +.plant-floor { + padding-bottom: 25px; +} + +.plant-floor-filter { + padding-top: 10px; + display: flex; + flex-wrap: wrap; +} + +.plant-floor-container { + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: var(--margin-xl); +} + +@media screen and (max-width: 620px) { + .plant-floor-container { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +.plant-floor-container .workstation-card { + padding: 5px; +} + +.plant-floor-container .workstation-image-link { + width: 100%; + font-size: 50px; + margin: var(--margin-sm); + min-height: 9rem; +} + +.workstation-abbr { + display: flex; + background-color: var(--control-bg); + height: 100%; + width: 100%; + align-items: center; + justify-content: center; +} + +.frappe-control[data-fieldname="other_charges_calculation"] .ql-editor { + white-space: normal; +} +>>>>>>> 967540da18 (fix: style for tax breakup) From 07c98146c5cb2fe25363435109c97a0923a6b273 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 3 Feb 2024 11:58:11 +0530 Subject: [PATCH 07/94] refactor: more options for 'status' and move it to top (cherry picked from commit 0d65d878deb717b80a22cae4d152d07289acae58) # Conflicts: # erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py --- .../transaction_deletion_record.json | 22 ++++++++++++++----- .../transaction_deletion_record.py | 22 +++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json index 23e59472a6d7..8f3a5d056662 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json @@ -7,10 +7,12 @@ "engine": "InnoDB", "field_order": [ "company", + "column_break_txbg", + "status", + "section_break_tbej", "doctypes", "doctypes_to_be_ignored", - "amended_from", - "status" + "amended_from" ], "fields": [ { @@ -46,18 +48,27 @@ { "fieldname": "status", "fieldtype": "Select", - "hidden": 1, "label": "Status", - "options": "Draft\nCompleted" + "options": "Queued\nRunning\nFailed\nCompleted\nCancelled", + "read_only": 1 + }, + { + "fieldname": "column_break_txbg", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_tbej", + "fieldtype": "Section Break" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-08-04 20:15:59.071493", + "modified": "2024-02-03 12:42:21.628177", "modified_by": "Administrator", "module": "Setup", "name": "Transaction Deletion Record", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -76,5 +87,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index 649b43b5e914..7f6e72db725f 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -10,6 +10,28 @@ class TransactionDeletionRecord(Document): +<<<<<<< HEAD +======= + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from erpnext.setup.doctype.transaction_deletion_record_item.transaction_deletion_record_item import ( + TransactionDeletionRecordItem, + ) + + amended_from: DF.Link | None + company: DF.Link + doctypes: DF.Table[TransactionDeletionRecordItem] + doctypes_to_be_ignored: DF.Table[TransactionDeletionRecordItem] + status: DF.Literal["Queued", "Running", "Completed"] + # end: auto-generated types + +>>>>>>> 0d65d878de (refactor: more options for 'status' and move it to top) def __init__(self, *args, **kwargs): super(TransactionDeletionRecord, self).__init__(*args, **kwargs) self.batch_size = 5000 From 7280a76f73f28c494522596783b409b3d721b69b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 3 Feb 2024 12:46:12 +0530 Subject: [PATCH 08/94] refactor: set status and trigger job on submit (cherry picked from commit 6fbb67b1d2da1fe9aa1879ce354f5702150584c8) --- .../transaction_deletion_record.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index 7f6e72db725f..cd7cd3ab3387 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -28,7 +28,7 @@ class TransactionDeletionRecord(Document): company: DF.Link doctypes: DF.Table[TransactionDeletionRecordItem] doctypes_to_be_ignored: DF.Table[TransactionDeletionRecordItem] - status: DF.Literal["Queued", "Running", "Completed"] + status: DF.Literal["Queued", "Running", "Failed", "Completed", "Cancelled"] # end: auto-generated types >>>>>>> 0d65d878de (refactor: more options for 'status' and move it to top) @@ -55,6 +55,16 @@ def before_submit(self): if not self.doctypes_to_be_ignored: self.populate_doctypes_to_be_ignored_table() + def before_save(self): + self.status = "" + + def on_submit(self): + self.db_set("status", "Queued") + + def on_cancel(self): + self.db_set("status", "Cancelled") + + def start_deletion_process(self): self.delete_bins() self.delete_lead_addresses() self.reset_company_values() From 52d22d8b557024b43f49b24ce299d0aa68aac7df Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 3 Feb 2024 14:29:04 +0530 Subject: [PATCH 09/94] refactor: tasks section and UI niceties (cherry picked from commit d0dc2c6e77c4e04e8d74a36a632ad6f2189dfedc) --- .../transaction_deletion_record.json | 53 ++++++++++++++++++- .../transaction_deletion_record.py | 19 +++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json index 8f3a5d056662..a9e04d3892e6 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json @@ -9,6 +9,12 @@ "company", "column_break_txbg", "status", + "tasks_section", + "delete_bin_data", + "delete_leads_and_addresses", + "reset_company_default_values", + "clear_notifications", + "delete_transactions", "section_break_tbej", "doctypes", "doctypes_to_be_ignored", @@ -59,12 +65,57 @@ { "fieldname": "section_break_tbej", "fieldtype": "Section Break" + }, + { + "fieldname": "tasks_section", + "fieldtype": "Section Break", + "label": "Tasks" + }, + { + "default": "0", + "fieldname": "delete_bin_data", + "fieldtype": "Check", + "label": "Delete Bins", + "no_copy": 1, + "read_only": 1 + }, + { + "default": "0", + "fieldname": "delete_leads_and_addresses", + "fieldtype": "Check", + "label": "Delete Leads and Addresses", + "no_copy": 1, + "read_only": 1 + }, + { + "default": "0", + "fieldname": "clear_notifications", + "fieldtype": "Check", + "label": "Clear Notifications", + "no_copy": 1, + "read_only": 1 + }, + { + "default": "0", + "fieldname": "reset_company_default_values", + "fieldtype": "Check", + "label": "Reset Company Default Values", + "no_copy": 1, + "read_only": 1 + }, + { + "default": "0", + "fieldname": "delete_transactions", + "fieldtype": "Check", + "label": "Delete Transactions", + "no_copy": 1, + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-02-03 12:42:21.628177", + "modified": "2024-02-03 14:40:40.207482", "modified_by": "Administrator", "module": "Setup", "name": "Transaction Deletion Record", diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index cd7cd3ab3387..c4015f262f51 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -25,9 +25,14 @@ class TransactionDeletionRecord(Document): ) amended_from: DF.Link | None + clear_notifications: DF.Check company: DF.Link + delete_bin_data: DF.Check + delete_leads_and_addresses: DF.Check + delete_transactions: DF.Check doctypes: DF.Table[TransactionDeletionRecordItem] doctypes_to_be_ignored: DF.Table[TransactionDeletionRecordItem] + reset_company_default_values: DF.Check status: DF.Literal["Queued", "Running", "Failed", "Completed", "Cancelled"] # end: auto-generated types @@ -55,8 +60,16 @@ def before_submit(self): if not self.doctypes_to_be_ignored: self.populate_doctypes_to_be_ignored_table() + def reset_task_flags(self): + self.clear_notifications = 0 + self.delete_bin_data = 0 + self.delete_leads_and_addresses = 0 + self.delete_transactions = 0 + self.reset_company_default_values = 0 + def before_save(self): self.status = "" + self.reset_task_flags() def on_submit(self): self.db_set("status", "Queued") @@ -64,11 +77,13 @@ def on_submit(self): def on_cancel(self): self.db_set("status", "Cancelled") + @frappe.whitelist() def start_deletion_process(self): self.delete_bins() self.delete_lead_addresses() self.reset_company_values() clear_notifications() + self.db_set("clear_notifications", 1) self.delete_company_transactions() def populate_doctypes_to_be_ignored_table(self): @@ -82,6 +97,7 @@ def delete_bins(self): (select name from tabWarehouse where company=%s)""", self.company, ) + self.db_set("delete_bin_data", 1) def delete_lead_addresses(self): """Delete addresses to which leads are linked""" @@ -120,12 +136,14 @@ def delete_lead_addresses(self): leads=",".join(leads) ) ) + self.db_set("delete_leads_and_addresses", 1) def reset_company_values(self): company_obj = frappe.get_doc("Company", self.company) company_obj.total_monthly_sales = 0 company_obj.sales_monthly_history = None company_obj.save() + self.db_set("reset_company_default_values", 1) def delete_company_transactions(self): doctypes_to_be_ignored_list = self.get_doctypes_to_be_ignored_list() @@ -159,6 +177,7 @@ def delete_company_transactions(self): if naming_series: if "#" in naming_series: self.update_naming_series(naming_series, docfield["parent"]) + self.db_set("delete_transactions", 1) def get_doctypes_to_be_ignored_list(self): singles = frappe.get_all("DocType", filters={"issingle": 1}, pluck="name") From 5542985e251e56e0a746bbe8dd850acb07fb66d4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 3 Feb 2024 14:49:18 +0530 Subject: [PATCH 10/94] refactor: UI trigger (cherry picked from commit 8944ab8b6ad1da2acdd5a852a17fc72e11d84695) --- .../transaction_deletion_record.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js index 527c753d6a97..671f927106d6 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js @@ -24,6 +24,17 @@ frappe.ui.form.on("Transaction Deletion Record", { refresh: function (frm) { frm.fields_dict["doctypes_to_be_ignored"].grid.set_column_disp("no_of_docs", false); frm.refresh_field("doctypes_to_be_ignored"); + + if (frm.doc.docstatus==1 && ['Queued', 'Failed'].find(x => x == frm.doc.status)) { + let execute_btn = __("Start / Resume") + + frm.add_custom_button(execute_btn, () => { + frm.call({ + method: 'start_deletion_process', + doc: frm.doc + }); + }); + } }, }); From c9d77044b0c0a13379b3b38c298287fca377b57e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 3 Feb 2024 20:21:30 +0530 Subject: [PATCH 11/94] refactor: use flags to decide on current stage (cherry picked from commit 6a77d86a53580b670937b92bcbde69ea920dbb9e) # Conflicts: # erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.py --- .../transaction_deletion_record.js | 12 +- .../transaction_deletion_record.json | 10 +- .../transaction_deletion_record.py | 196 ++++++++++-------- .../transaction_deletion_record_item.json | 19 +- .../transaction_deletion_record_item.py | 20 ++ 5 files changed, 159 insertions(+), 98 deletions(-) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js index 671f927106d6..1a8b52f46bd5 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js @@ -16,9 +16,15 @@ frappe.ui.form.on("Transaction Deletion Record", { }); } - frm.get_field("doctypes_to_be_ignored").grid.cannot_add_rows = true; - frm.fields_dict["doctypes_to_be_ignored"].grid.set_column_disp("no_of_docs", false); - frm.refresh_field("doctypes_to_be_ignored"); + + frm.get_field('doctypes_to_be_ignored').grid.cannot_add_rows = true; + frm.fields_dict['doctypes_to_be_ignored'].grid.set_column_disp('no_of_docs', false); + frm.fields_dict['doctypes_to_be_ignored'].grid.set_column_disp('done', false); + frm.refresh_field('doctypes_to_be_ignored'); + + frm.get_field('doctypes').grid.cannot_add_rows = true; + frm.fields_dict['doctypes'].grid.set_column_disp('no_of_docs', true); + frm.refresh_field('doctypes'); }, refresh: function (frm) { diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json index a9e04d3892e6..6a848413ffc6 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json @@ -15,6 +15,7 @@ "reset_company_default_values", "clear_notifications", "delete_transactions", + "initialize_doctypes_table", "section_break_tbej", "doctypes", "doctypes_to_be_ignored", @@ -110,12 +111,19 @@ "label": "Delete Transactions", "no_copy": 1, "read_only": 1 + }, + { + "default": "0", + "fieldname": "initialize_doctypes_table", + "fieldtype": "Check", + "label": "Initialize Summary Table", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-02-03 14:40:40.207482", + "modified": "2024-02-03 20:48:34.107577", "modified_by": "Administrator", "module": "Setup", "name": "Transaction Deletion Record", diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index c4015f262f51..55837ac385c3 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -32,6 +32,7 @@ class TransactionDeletionRecord(Document): delete_transactions: DF.Check doctypes: DF.Table[TransactionDeletionRecordItem] doctypes_to_be_ignored: DF.Table[TransactionDeletionRecordItem] + initialize_doctypes_table: DF.Check reset_company_default_values: DF.Check status: DF.Literal["Queued", "Running", "Failed", "Completed", "Cancelled"] # end: auto-generated types @@ -82,8 +83,10 @@ def start_deletion_process(self): self.delete_bins() self.delete_lead_addresses() self.reset_company_values() - clear_notifications() - self.db_set("clear_notifications", 1) + if not self.clear_notifications: + clear_notifications() + self.db_set("clear_notifications", 1) + self.initialize_doctypes_to_be_deleted_table() self.delete_company_transactions() def populate_doctypes_to_be_ignored_table(self): @@ -92,92 +95,108 @@ def populate_doctypes_to_be_ignored_table(self): self.append("doctypes_to_be_ignored", {"doctype_name": doctype}) def delete_bins(self): - frappe.db.sql( - """delete from `tabBin` where warehouse in - (select name from tabWarehouse where company=%s)""", - self.company, - ) - self.db_set("delete_bin_data", 1) + if not self.delete_bin_data: + frappe.db.sql( + """delete from `tabBin` where warehouse in + (select name from tabWarehouse where company=%s)""", + self.company, + ) + self.db_set("delete_bin_data", 1) def delete_lead_addresses(self): """Delete addresses to which leads are linked""" - leads = frappe.get_all("Lead", filters={"company": self.company}) - leads = ["'%s'" % row.get("name") for row in leads] - addresses = [] - if leads: - addresses = frappe.db.sql_list( - """select parent from `tabDynamic Link` where link_name - in ({leads})""".format( - leads=",".join(leads) + if not self.delete_leads_and_addresses: + leads = frappe.get_all("Lead", filters={"company": self.company}) + leads = ["'%s'" % row.get("name") for row in leads] + addresses = [] + if leads: + addresses = frappe.db.sql_list( + """select parent from `tabDynamic Link` where link_name + in ({leads})""".format( + leads=",".join(leads) + ) ) - ) - if addresses: - addresses = ["%s" % frappe.db.escape(addr) for addr in addresses] + if addresses: + addresses = ["%s" % frappe.db.escape(addr) for addr in addresses] - frappe.db.sql( - """delete from `tabAddress` where name in ({addresses}) and - name not in (select distinct dl1.parent from `tabDynamic Link` dl1 - inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent - and dl1.link_doctype<>dl2.link_doctype)""".format( - addresses=",".join(addresses) + frappe.db.sql( + """delete from `tabAddress` where name in ({addresses}) and + name not in (select distinct dl1.parent from `tabDynamic Link` dl1 + inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent + and dl1.link_doctype<>dl2.link_doctype)""".format( + addresses=",".join(addresses) + ) + ) + + frappe.db.sql( + """delete from `tabDynamic Link` where link_doctype='Lead' + and parenttype='Address' and link_name in ({leads})""".format( + leads=",".join(leads) + ) ) - ) frappe.db.sql( - """delete from `tabDynamic Link` where link_doctype='Lead' - and parenttype='Address' and link_name in ({leads})""".format( + """update `tabCustomer` set lead_name=NULL where lead_name in ({leads})""".format( leads=",".join(leads) ) ) - - frappe.db.sql( - """update `tabCustomer` set lead_name=NULL where lead_name in ({leads})""".format( - leads=",".join(leads) - ) - ) - self.db_set("delete_leads_and_addresses", 1) + self.db_set("delete_leads_and_addresses", 1) def reset_company_values(self): - company_obj = frappe.get_doc("Company", self.company) - company_obj.total_monthly_sales = 0 - company_obj.sales_monthly_history = None - company_obj.save() - self.db_set("reset_company_default_values", 1) + if not self.reset_company_default_values: + company_obj = frappe.get_doc("Company", self.company) + company_obj.total_monthly_sales = 0 + company_obj.sales_monthly_history = None + company_obj.save() + self.db_set("reset_company_default_values", 1) + + def initialize_doctypes_to_be_deleted_table(self): + if not self.initialize_doctypes_table: + doctypes_to_be_ignored_list = self.get_doctypes_to_be_ignored_list() + docfields = self.get_doctypes_with_company_field(doctypes_to_be_ignored_list) + tables = self.get_all_child_doctypes() + for docfield in docfields: + if docfield["parent"] != self.doctype: + no_of_docs = self.get_number_of_docs_linked_with_specified_company( + docfield["parent"], docfield["fieldname"] + ) + if no_of_docs > 0: + # Initialize + self.populate_doctypes_table(tables, docfield["parent"], docfield["fieldname"], 0) + self.db_set("initialize_doctypes_table", 1) def delete_company_transactions(self): - doctypes_to_be_ignored_list = self.get_doctypes_to_be_ignored_list() - docfields = self.get_doctypes_with_company_field(doctypes_to_be_ignored_list) - - tables = self.get_all_child_doctypes() - for docfield in docfields: - if docfield["parent"] != self.doctype: - no_of_docs = self.get_number_of_docs_linked_with_specified_company( - docfield["parent"], docfield["fieldname"] - ) - - if no_of_docs > 0: - self.delete_version_log(docfield["parent"], docfield["fieldname"]) - - reference_docs = frappe.get_all( - docfield["parent"], filters={docfield["fieldname"]: self.company} + if not self.delete_transactions: + doctypes_to_be_ignored_list = self.get_doctypes_to_be_ignored_list() + docfields = self.get_doctypes_with_company_field(doctypes_to_be_ignored_list) + + tables = self.get_all_child_doctypes() + for docfield in self.doctypes: + if docfield.doctype_name != self.doctype: + no_of_docs = self.get_number_of_docs_linked_with_specified_company( + docfield.doctype_name, docfield.docfield_name ) - reference_doc_names = [r.name for r in reference_docs] - - self.delete_communications(docfield["parent"], reference_doc_names) - self.delete_comments(docfield["parent"], reference_doc_names) - self.unlink_attachments(docfield["parent"], reference_doc_names) - - self.populate_doctypes_table(tables, docfield["parent"], no_of_docs) - - self.delete_child_tables(docfield["parent"], docfield["fieldname"]) - self.delete_docs_linked_with_specified_company(docfield["parent"], docfield["fieldname"]) - - naming_series = frappe.db.get_value("DocType", docfield["parent"], "autoname") - if naming_series: - if "#" in naming_series: - self.update_naming_series(naming_series, docfield["parent"]) - self.db_set("delete_transactions", 1) + if no_of_docs > 0: + reference_docs = frappe.get_all( + docfield.doctype_name, filters={docfield.docfield_name: self.company}, limit=self.batch_size + ) + reference_doc_names = [r.name for r in reference_docs] + + self.delete_version_log(docfield.doctype_name, reference_doc_names) + self.delete_communications(docfield.doctype_name, reference_doc_names) + self.delete_comments(docfield.doctype_name, reference_doc_names) + self.unlink_attachments(docfield.doctype_name, reference_doc_names) + + self.delete_child_tables(docfield.doctype_name, reference_doc_names) + self.delete_docs_linked_with_specified_company(docfield.doctype_name, docfield.docfield_name) + + naming_series = frappe.db.get_value("DocType", docfield.doctype_name, "autoname") + # TODO: do this at the end of each doctype + if naming_series: + if "#" in naming_series: + self.update_naming_series(naming_series, docfield.doctype_name) + self.db_set("delete_transactions", 1) def get_doctypes_to_be_ignored_list(self): singles = frappe.get_all("DocType", filters={"issingle": 1}, pluck="name") @@ -206,22 +225,21 @@ def get_all_child_doctypes(self): def get_number_of_docs_linked_with_specified_company(self, doctype, company_fieldname): return frappe.db.count(doctype, {company_fieldname: self.company}) - def populate_doctypes_table(self, tables, doctype, no_of_docs): + def populate_doctypes_table(self, tables, doctype, fieldname, no_of_docs): + self.flags.ignore_validate_update_after_submit = True if doctype not in tables: - self.append("doctypes", {"doctype_name": doctype, "no_of_docs": no_of_docs}) - - def delete_child_tables(self, doctype, company_fieldname): - parent_docs_to_be_deleted = frappe.get_all( - doctype, {company_fieldname: self.company}, pluck="name" - ) + self.append( + "doctypes", {"doctype_name": doctype, "docfield_name": fieldname, "no_of_docs": no_of_docs} + ) + self.save(ignore_permissions=True) + def delete_child_tables(self, doctype, reference_doc_names): child_tables = frappe.get_all( "DocField", filters={"fieldtype": "Table", "parent": doctype}, pluck="options" ) - for batch in create_batch(parent_docs_to_be_deleted, self.batch_size): - for table in child_tables: - frappe.db.delete(table, {"parent": ["in", batch]}) + for table in child_tables: + frappe.db.delete(table, {"parent": ["in", reference_doc_names]}) def delete_docs_linked_with_specified_company(self, doctype, company_fieldname): frappe.db.delete(doctype, {company_fieldname: self.company}) @@ -245,17 +263,11 @@ def update_naming_series(self, naming_series, doctype_name): frappe.db.sql("""update `tabSeries` set current = %s where name=%s""", (last, prefix)) - def delete_version_log(self, doctype, company_fieldname): - dt = qb.DocType(doctype) - names = qb.from_(dt).select(dt.name).where(dt[company_fieldname] == self.company).run(as_list=1) - names = [x[0] for x in names] - - if names: - versions = qb.DocType("Version") - for batch in create_batch(names, self.batch_size): - qb.from_(versions).delete().where( - (versions.ref_doctype == doctype) & (versions.docname.isin(batch)) - ).run() + def delete_version_log(self, doctype, docnames): + versions = qb.DocType("Version") + qb.from_(versions).delete().where( + (versions.ref_doctype == doctype) & (versions.docname.isin(docnames)) + ).run() def delete_communications(self, doctype, reference_doc_names): communications = frappe.get_all( diff --git a/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.json b/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.json index be0be945c4e6..4e5e18469997 100644 --- a/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.json +++ b/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.json @@ -6,7 +6,9 @@ "engine": "InnoDB", "field_order": [ "doctype_name", - "no_of_docs" + "docfield_name", + "no_of_docs", + "done" ], "fields": [ { @@ -22,12 +24,24 @@ "fieldtype": "Data", "in_list_view": 1, "label": "Number of Docs" + }, + { + "default": "0", + "fieldname": "done", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Done" + }, + { + "fieldname": "docfield_name", + "fieldtype": "Data", + "label": "DocField Name" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-05-08 23:10:46.166744", + "modified": "2024-02-03 21:06:32.274445", "modified_by": "Administrator", "module": "Setup", "name": "Transaction Deletion Record Item", @@ -35,5 +49,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.py b/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.py index 92ca8a2ac730..c004ed7a09d8 100644 --- a/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.py +++ b/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.py @@ -7,4 +7,24 @@ class TransactionDeletionRecordItem(Document): +<<<<<<< HEAD +======= + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + docfield_name: DF.Data | None + doctype_name: DF.Link + done: DF.Check + no_of_docs: DF.Data | None + parent: DF.Data + parentfield: DF.Data + parenttype: DF.Data + # end: auto-generated types + +>>>>>>> 6a77d86a53 (refactor: use flags to decide on current stage) pass From e56138ddb00f11e0b3708c172977a7320c5c2a42 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 4 Feb 2024 08:09:53 +0530 Subject: [PATCH 12/94] refactor: reorder flags in Tasks section (cherry picked from commit cccb2d5141e30234dd9d3f7ff877c9aaaa44879e) --- .../transaction_deletion_record.json | 4 ++-- .../transaction_deletion_record.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json index 6a848413ffc6..dc35fe595535 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json @@ -14,8 +14,8 @@ "delete_leads_and_addresses", "reset_company_default_values", "clear_notifications", - "delete_transactions", "initialize_doctypes_table", + "delete_transactions", "section_break_tbej", "doctypes", "doctypes_to_be_ignored", @@ -123,7 +123,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-02-03 20:48:34.107577", + "modified": "2024-02-04 08:09:26.784109", "modified_by": "Administrator", "module": "Setup", "name": "Transaction Deletion Record", diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index 55837ac385c3..2bbb515498d8 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -173,7 +173,7 @@ def delete_company_transactions(self): tables = self.get_all_child_doctypes() for docfield in self.doctypes: - if docfield.doctype_name != self.doctype: + if docfield.doctype_name != self.doctype and not docfield.done: no_of_docs = self.get_number_of_docs_linked_with_specified_company( docfield.doctype_name, docfield.docfield_name ) @@ -196,6 +196,9 @@ def delete_company_transactions(self): if naming_series: if "#" in naming_series: self.update_naming_series(naming_series, docfield.doctype_name) + + else: + frappe.db.set_value(docfield.doctype, docfield.name, "done", 1) self.db_set("delete_transactions", 1) def get_doctypes_to_be_ignored_list(self): From b1367e839ccf98164bdc7bb591dba2a150729df7 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 4 Feb 2024 08:25:18 +0530 Subject: [PATCH 13/94] refactor: chained callback (cherry picked from commit b12ca65fcc76c85c9a26fb9d6d2e252ede4a3e70) --- .../transaction_deletion_record.py | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index 2bbb515498d8..b897c7bf879f 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -78,22 +78,37 @@ def on_submit(self): def on_cancel(self): self.db_set("status", "Cancelled") + def chain_callback(self, method): + frappe.enqueue( + "frappe.utils.background_jobs.run_doc_method", + doctype=self.doctype, + name=self.name, + doc_method=method, + queue="long", + enqueue_after_commit=True, + ) + @frappe.whitelist() def start_deletion_process(self): self.delete_bins() self.delete_lead_addresses() self.reset_company_values() + self.delete_notifications() + self.initialize_doctypes_to_be_deleted_table() + self.delete_company_transactions() + + def delete_notifications(self): if not self.clear_notifications: clear_notifications() self.db_set("clear_notifications", 1) - self.initialize_doctypes_to_be_deleted_table() - self.delete_company_transactions() + self.chain_callback("initialize_doctypes_to_be_deleted_table") def populate_doctypes_to_be_ignored_table(self): doctypes_to_be_ignored_list = get_doctypes_to_be_ignored() for doctype in doctypes_to_be_ignored_list: self.append("doctypes_to_be_ignored", {"doctype_name": doctype}) + @frappe.whitelist() def delete_bins(self): if not self.delete_bin_data: frappe.db.sql( @@ -102,6 +117,7 @@ def delete_bins(self): self.company, ) self.db_set("delete_bin_data", 1) + self.chain_callback(method="delete_lead_addresses") def delete_lead_addresses(self): """Delete addresses to which leads are linked""" @@ -142,6 +158,7 @@ def delete_lead_addresses(self): ) ) self.db_set("delete_leads_and_addresses", 1) + self.chain_callback(method="reset_company_values") def reset_company_values(self): if not self.reset_company_default_values: @@ -150,6 +167,7 @@ def reset_company_values(self): company_obj.sales_monthly_history = None company_obj.save() self.db_set("reset_company_default_values", 1) + self.chain_callback(method="delete_notifications") def initialize_doctypes_to_be_deleted_table(self): if not self.initialize_doctypes_table: @@ -165,6 +183,7 @@ def initialize_doctypes_to_be_deleted_table(self): # Initialize self.populate_doctypes_table(tables, docfield["parent"], docfield["fieldname"], 0) self.db_set("initialize_doctypes_table", 1) + self.chain_callback(method="delete_company_transactions") def delete_company_transactions(self): if not self.delete_transactions: @@ -197,6 +216,7 @@ def delete_company_transactions(self): if "#" in naming_series: self.update_naming_series(naming_series, docfield.doctype_name) + self.chain_callback(method="delete_company_transactions") else: frappe.db.set_value(docfield.doctype, docfield.name, "done", 1) self.db_set("delete_transactions", 1) From 0d791f594f91e7884d7ef368f4954f847a455342 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 4 Feb 2024 10:57:34 +0530 Subject: [PATCH 14/94] refactor: use separate child table for summary (cherry picked from commit 49d3bcbc8df8fe9457cce55793896146705b50e5) --- .../__init__.py | 0 .../transaction_deletion_record_details.json | 59 +++++++++++++++++++ .../transaction_deletion_record_details.py | 26 ++++++++ .../transaction_deletion_record.js | 12 ++++ .../transaction_deletion_record.json | 4 +- .../transaction_deletion_record.py | 43 ++++++++------ .../transaction_deletion_record_item.json | 25 +------- .../transaction_deletion_record_item.py | 3 - 8 files changed, 126 insertions(+), 46 deletions(-) create mode 100644 erpnext/accounts/doctype/transaction_deletion_record_details/__init__.py create mode 100644 erpnext/accounts/doctype/transaction_deletion_record_details/transaction_deletion_record_details.json create mode 100644 erpnext/accounts/doctype/transaction_deletion_record_details/transaction_deletion_record_details.py diff --git a/erpnext/accounts/doctype/transaction_deletion_record_details/__init__.py b/erpnext/accounts/doctype/transaction_deletion_record_details/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/accounts/doctype/transaction_deletion_record_details/transaction_deletion_record_details.json b/erpnext/accounts/doctype/transaction_deletion_record_details/transaction_deletion_record_details.json new file mode 100644 index 000000000000..e8a5eb6c4321 --- /dev/null +++ b/erpnext/accounts/doctype/transaction_deletion_record_details/transaction_deletion_record_details.json @@ -0,0 +1,59 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-02-04 10:53:32.307930", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "doctype_name", + "docfield_name", + "no_of_docs", + "done" + ], + "fields": [ + { + "fieldname": "doctype_name", + "fieldtype": "Link", + "in_list_view": 1, + "label": "DocType", + "options": "DocType", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "docfield_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "DocField", + "read_only": 1 + }, + { + "fieldname": "no_of_docs", + "fieldtype": "Int", + "in_list_view": 1, + "label": "No of Docs", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "done", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Done", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2024-02-04 10:55:52.060417", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Transaction Deletion Record Details", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/transaction_deletion_record_details/transaction_deletion_record_details.py b/erpnext/accounts/doctype/transaction_deletion_record_details/transaction_deletion_record_details.py new file mode 100644 index 000000000000..bc5b5c41fddb --- /dev/null +++ b/erpnext/accounts/doctype/transaction_deletion_record_details/transaction_deletion_record_details.py @@ -0,0 +1,26 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class TransactionDeletionRecordDetails(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + docfield_name: DF.Data | None + doctype_name: DF.Link + done: DF.Check + no_of_docs: DF.Int + parent: DF.Data + parentfield: DF.Data + parenttype: DF.Data + # end: auto-generated types + + pass diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js index 1a8b52f46bd5..c6bb3781bfcf 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js @@ -41,6 +41,18 @@ frappe.ui.form.on("Transaction Deletion Record", { }); }); } + + if (frm.doc.docstatus==1 && ['Queued', 'Failed'].find(x => x == frm.doc.status)) { + let execute_btn = __("Start Chain of Events") + + frm.add_custom_button(execute_btn, () => { + frm.call({ + method: 'delete_bins', + doc: frm.doc + }); + }); + } + }, }); diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json index dc35fe595535..bbc571a0816a 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json @@ -34,7 +34,7 @@ "fieldname": "doctypes", "fieldtype": "Table", "label": "Summary", - "options": "Transaction Deletion Record Item", + "options": "Transaction Deletion Record Details", "read_only": 1 }, { @@ -123,7 +123,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-02-04 08:09:26.784109", + "modified": "2024-02-04 10:55:09.430373", "modified_by": "Administrator", "module": "Setup", "name": "Transaction Deletion Record", diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index b897c7bf879f..c7efeaab146b 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -20,6 +20,9 @@ class TransactionDeletionRecord(Document): if TYPE_CHECKING: from frappe.types import DF + from erpnext.accounts.doctype.transaction_deletion_record_details.transaction_deletion_record_details import ( + TransactionDeletionRecordDetails, + ) from erpnext.setup.doctype.transaction_deletion_record_item.transaction_deletion_record_item import ( TransactionDeletionRecordItem, ) @@ -30,7 +33,7 @@ class TransactionDeletionRecord(Document): delete_bin_data: DF.Check delete_leads_and_addresses: DF.Check delete_transactions: DF.Check - doctypes: DF.Table[TransactionDeletionRecordItem] + doctypes: DF.Table[TransactionDeletionRecordDetails] doctypes_to_be_ignored: DF.Table[TransactionDeletionRecordItem] initialize_doctypes_table: DF.Check reset_company_default_values: DF.Check @@ -40,7 +43,7 @@ class TransactionDeletionRecord(Document): >>>>>>> 0d65d878de (refactor: more options for 'status' and move it to top) def __init__(self, *args, **kwargs): super(TransactionDeletionRecord, self).__init__(*args, **kwargs) - self.batch_size = 5000 + self.batch_size = 5 def validate(self): frappe.only_for("System Manager") @@ -78,7 +81,7 @@ def on_submit(self): def on_cancel(self): self.db_set("status", "Cancelled") - def chain_callback(self, method): + def chain_call(self, method): frappe.enqueue( "frappe.utils.background_jobs.run_doc_method", doctype=self.doctype, @@ -101,7 +104,7 @@ def delete_notifications(self): if not self.clear_notifications: clear_notifications() self.db_set("clear_notifications", 1) - self.chain_callback("initialize_doctypes_to_be_deleted_table") + self.chain_call("initialize_doctypes_to_be_deleted_table") def populate_doctypes_to_be_ignored_table(self): doctypes_to_be_ignored_list = get_doctypes_to_be_ignored() @@ -117,7 +120,7 @@ def delete_bins(self): self.company, ) self.db_set("delete_bin_data", 1) - self.chain_callback(method="delete_lead_addresses") + self.chain_call(method="delete_lead_addresses") def delete_lead_addresses(self): """Delete addresses to which leads are linked""" @@ -158,7 +161,7 @@ def delete_lead_addresses(self): ) ) self.db_set("delete_leads_and_addresses", 1) - self.chain_callback(method="reset_company_values") + self.chain_call(method="reset_company_values") def reset_company_values(self): if not self.reset_company_default_values: @@ -167,7 +170,7 @@ def reset_company_values(self): company_obj.sales_monthly_history = None company_obj.save() self.db_set("reset_company_default_values", 1) - self.chain_callback(method="delete_notifications") + self.chain_call(method="delete_notifications") def initialize_doctypes_to_be_deleted_table(self): if not self.initialize_doctypes_table: @@ -183,7 +186,7 @@ def initialize_doctypes_to_be_deleted_table(self): # Initialize self.populate_doctypes_table(tables, docfield["parent"], docfield["fieldname"], 0) self.db_set("initialize_doctypes_table", 1) - self.chain_callback(method="delete_company_transactions") + self.chain_call(method="delete_company_transactions") def delete_company_transactions(self): if not self.delete_transactions: @@ -206,20 +209,24 @@ def delete_company_transactions(self): self.delete_communications(docfield.doctype_name, reference_doc_names) self.delete_comments(docfield.doctype_name, reference_doc_names) self.unlink_attachments(docfield.doctype_name, reference_doc_names) - self.delete_child_tables(docfield.doctype_name, reference_doc_names) - self.delete_docs_linked_with_specified_company(docfield.doctype_name, docfield.docfield_name) - + self.delete_docs_linked_with_specified_company(docfield.doctype_name, reference_doc_names) + processed = int(docfield.no_of_docs) + len(reference_doc_names) + frappe.db.set_value(docfield.doctype, docfield.name, "no_of_docs", processed) + else: naming_series = frappe.db.get_value("DocType", docfield.doctype_name, "autoname") - # TODO: do this at the end of each doctype if naming_series: if "#" in naming_series: self.update_naming_series(naming_series, docfield.doctype_name) - - self.chain_callback(method="delete_company_transactions") - else: frappe.db.set_value(docfield.doctype, docfield.name, "done", 1) - self.db_set("delete_transactions", 1) + + pending_doctypes = frappe.db.get_all( + docfield.doctype, filters={"parent": self.name, "done": 0}, pluck="doctype_name" + ) + if pending_doctypes: + self.chain_call(method="delete_company_transactions") + else: + self.db_set("delete_transactions", 1) def get_doctypes_to_be_ignored_list(self): singles = frappe.get_all("DocType", filters={"issingle": 1}, pluck="name") @@ -264,8 +271,8 @@ def delete_child_tables(self, doctype, reference_doc_names): for table in child_tables: frappe.db.delete(table, {"parent": ["in", reference_doc_names]}) - def delete_docs_linked_with_specified_company(self, doctype, company_fieldname): - frappe.db.delete(doctype, {company_fieldname: self.company}) + def delete_docs_linked_with_specified_company(self, doctype, reference_doc_names): + frappe.db.delete(doctype, {"name": ("in", reference_doc_names)}) def update_naming_series(self, naming_series, doctype_name): if "." in naming_series: diff --git a/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.json b/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.json index 4e5e18469997..89db63694c20 100644 --- a/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.json +++ b/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.json @@ -5,10 +5,7 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "doctype_name", - "docfield_name", - "no_of_docs", - "done" + "doctype_name" ], "fields": [ { @@ -18,30 +15,12 @@ "label": "DocType", "options": "DocType", "reqd": 1 - }, - { - "fieldname": "no_of_docs", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Number of Docs" - }, - { - "default": "0", - "fieldname": "done", - "fieldtype": "Check", - "in_list_view": 1, - "label": "Done" - }, - { - "fieldname": "docfield_name", - "fieldtype": "Data", - "label": "DocField Name" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-02-03 21:06:32.274445", + "modified": "2024-02-04 10:56:27.413691", "modified_by": "Administrator", "module": "Setup", "name": "Transaction Deletion Record Item", diff --git a/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.py b/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.py index c004ed7a09d8..ed01afe70f94 100644 --- a/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.py +++ b/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.py @@ -17,10 +17,7 @@ class TransactionDeletionRecordItem(Document): if TYPE_CHECKING: from frappe.types import DF - docfield_name: DF.Data | None doctype_name: DF.Link - done: DF.Check - no_of_docs: DF.Data | None parent: DF.Data parentfield: DF.Data parenttype: DF.Data From a303788c715120019cdd387928d2595928529907 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 4 Feb 2024 15:26:33 +0530 Subject: [PATCH 15/94] chore: remove unwanted UI code (cherry picked from commit b98a5e4edcd896b05e4e7fd5874c56ceb9d95bf0) --- .../transaction_deletion_record.js | 29 +++---------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js index c6bb3781bfcf..027bbcb4b208 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js @@ -10,40 +10,19 @@ frappe.ui.form.on("Transaction Deletion Record", { callback: function (r) { doctypes_to_be_ignored_array = r.message; populate_doctypes_to_be_ignored(doctypes_to_be_ignored_array, frm); - frm.fields_dict["doctypes_to_be_ignored"].grid.set_column_disp("no_of_docs", false); - frm.refresh_field("doctypes_to_be_ignored"); - }, + frm.refresh_field('doctypes_to_be_ignored'); + } }); } frm.get_field('doctypes_to_be_ignored').grid.cannot_add_rows = true; - frm.fields_dict['doctypes_to_be_ignored'].grid.set_column_disp('no_of_docs', false); - frm.fields_dict['doctypes_to_be_ignored'].grid.set_column_disp('done', false); - frm.refresh_field('doctypes_to_be_ignored'); - frm.get_field('doctypes').grid.cannot_add_rows = true; - frm.fields_dict['doctypes'].grid.set_column_disp('no_of_docs', true); - frm.refresh_field('doctypes'); }, - refresh: function (frm) { - frm.fields_dict["doctypes_to_be_ignored"].grid.set_column_disp("no_of_docs", false); - frm.refresh_field("doctypes_to_be_ignored"); - - if (frm.doc.docstatus==1 && ['Queued', 'Failed'].find(x => x == frm.doc.status)) { - let execute_btn = __("Start / Resume") - - frm.add_custom_button(execute_btn, () => { - frm.call({ - method: 'start_deletion_process', - doc: frm.doc - }); - }); - } - + refresh: function(frm) { if (frm.doc.docstatus==1 && ['Queued', 'Failed'].find(x => x == frm.doc.status)) { - let execute_btn = __("Start Chain of Events") + let execute_btn = __("Start Deletion") frm.add_custom_button(execute_btn, () => { frm.call({ From 833df2c76b6e41aa52a05d6a463ec95272d1f8aa Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 4 Feb 2024 15:29:39 +0530 Subject: [PATCH 16/94] refactor: make Excluded doctype table read only (cherry picked from commit 7c4cff2649daa52d6cabf8a046b3867ab60e0da6) --- .../transaction_deletion_record.js | 4 +--- .../transaction_deletion_record.json | 5 +++-- .../transaction_deletion_record.py | 1 + 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js index 027bbcb4b208..ed70ebb5f700 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js @@ -15,9 +15,6 @@ frappe.ui.form.on("Transaction Deletion Record", { }); } - - frm.get_field('doctypes_to_be_ignored').grid.cannot_add_rows = true; - }, refresh: function(frm) { @@ -25,6 +22,7 @@ frappe.ui.form.on("Transaction Deletion Record", { let execute_btn = __("Start Deletion") frm.add_custom_button(execute_btn, () => { + // Entry point for chain of events frm.call({ method: 'delete_bins', doc: frm.doc diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json index bbc571a0816a..bd45b1c109df 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json @@ -41,7 +41,8 @@ "fieldname": "doctypes_to_be_ignored", "fieldtype": "Table", "label": "Excluded DocTypes", - "options": "Transaction Deletion Record Item" + "options": "Transaction Deletion Record Item", + "read_only": 1 }, { "fieldname": "amended_from", @@ -123,7 +124,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-02-04 10:55:09.430373", + "modified": "2024-02-04 15:28:29.532826", "modified_by": "Administrator", "module": "Setup", "name": "Transaction Deletion Record", diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index c7efeaab146b..89471071e799 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -113,6 +113,7 @@ def populate_doctypes_to_be_ignored_table(self): @frappe.whitelist() def delete_bins(self): + # This methid is the entry point for the chain of events that follow if not self.delete_bin_data: frappe.db.sql( """delete from `tabBin` where warehouse in From 341e467056c86e5b8c70c7b8dac9a66931bc64f3 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 4 Feb 2024 15:37:21 +0530 Subject: [PATCH 17/94] refactor: validate status before running events (cherry picked from commit 86b5e2e2779eb498a8525ca07c6e25335e6f2f5d) --- .../transaction_deletion_record.py | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index 89471071e799..15a9ca9d2cfb 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -6,7 +6,7 @@ from frappe import _, qb from frappe.desk.notifications import clear_notifications from frappe.model.document import Document -from frappe.utils import cint, create_batch +from frappe.utils import cint, create_batch, get_link_to_form class TransactionDeletionRecord(Document): @@ -43,7 +43,7 @@ class TransactionDeletionRecord(Document): >>>>>>> 0d65d878de (refactor: more options for 'status' and move it to top) def __init__(self, *args, **kwargs): super(TransactionDeletionRecord, self).__init__(*args, **kwargs) - self.batch_size = 5 + self.batch_size = 5000 def validate(self): frappe.only_for("System Manager") @@ -101,6 +101,7 @@ def start_deletion_process(self): self.delete_company_transactions() def delete_notifications(self): + self.validate_doc_status() if not self.clear_notifications: clear_notifications() self.db_set("clear_notifications", 1) @@ -111,9 +112,19 @@ def populate_doctypes_to_be_ignored_table(self): for doctype in doctypes_to_be_ignored_list: self.append("doctypes_to_be_ignored", {"doctype_name": doctype}) + def validate_doc_status(self): + if self.status != "Running": + frappe.throw( + _("{0} is not running. Cannot trigger events for this Document").format( + get_link_to_form("Transaction Deletion Record", self.name) + ) + ) + @frappe.whitelist() def delete_bins(self): # This methid is the entry point for the chain of events that follow + self.db_set("status", "Running") + if not self.delete_bin_data: frappe.db.sql( """delete from `tabBin` where warehouse in @@ -125,6 +136,7 @@ def delete_bins(self): def delete_lead_addresses(self): """Delete addresses to which leads are linked""" + self.validate_doc_status() if not self.delete_leads_and_addresses: leads = frappe.get_all("Lead", filters={"company": self.company}) leads = ["'%s'" % row.get("name") for row in leads] @@ -165,6 +177,7 @@ def delete_lead_addresses(self): self.chain_call(method="reset_company_values") def reset_company_values(self): + self.validate_doc_status() if not self.reset_company_default_values: company_obj = frappe.get_doc("Company", self.company) company_obj.total_monthly_sales = 0 @@ -174,6 +187,7 @@ def reset_company_values(self): self.chain_call(method="delete_notifications") def initialize_doctypes_to_be_deleted_table(self): + self.validate_doc_status() if not self.initialize_doctypes_table: doctypes_to_be_ignored_list = self.get_doctypes_to_be_ignored_list() docfields = self.get_doctypes_with_company_field(doctypes_to_be_ignored_list) @@ -190,6 +204,7 @@ def initialize_doctypes_to_be_deleted_table(self): self.chain_call(method="delete_company_transactions") def delete_company_transactions(self): + self.validate_doc_status() if not self.delete_transactions: doctypes_to_be_ignored_list = self.get_doctypes_to_be_ignored_list() docfields = self.get_doctypes_with_company_field(doctypes_to_be_ignored_list) @@ -215,6 +230,7 @@ def delete_company_transactions(self): processed = int(docfield.no_of_docs) + len(reference_doc_names) frappe.db.set_value(docfield.doctype, docfield.name, "no_of_docs", processed) else: + # reset naming series naming_series = frappe.db.get_value("DocType", docfield.doctype_name, "autoname") if naming_series: if "#" in naming_series: @@ -227,6 +243,7 @@ def delete_company_transactions(self): if pending_doctypes: self.chain_call(method="delete_company_transactions") else: + self.db_set("status", "Completed") self.db_set("delete_transactions", 1) def get_doctypes_to_be_ignored_list(self): From c38cfd14f3d3ba16e358ee364ef2772c5fb4bd9a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 4 Feb 2024 16:11:42 +0530 Subject: [PATCH 18/94] chore: show correct status in list view (cherry picked from commit 1014940953ed54f31a170e0e99d03c1c5f1cd022) --- .../transaction_deletion_record_list.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record_list.js b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record_list.js index 08a35df2c174..7c7b8ff25a72 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record_list.js +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record_list.js @@ -1,12 +1,16 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.listview_settings["Transaction Deletion Record"] = { - get_indicator: function (doc) { - if (doc.docstatus == 0) { - return [__("Draft"), "red"]; - } else { - return [__("Completed"), "green"]; - } +frappe.listview_settings['Transaction Deletion Record'] = { + add_fields: ["status"], + get_indicator: function(doc) { + let colors = { + 'Queued': 'orange', + 'Completed': 'green', + 'Running': 'blue', + 'Failed': 'red', + }; + let status = doc.status; + return [__(status), colors[status], 'status,=,'+status]; }, }; From 97ed9056274957b055025267fbb040ae09d7974b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 5 Feb 2024 10:21:12 +0530 Subject: [PATCH 19/94] refactor: reset all flags and remove unwanted code (cherry picked from commit 2dbe68a09d0bf9a13ef0d3bb8f071a7634291766) --- .../transaction_deletion_record.json | 4 +++- .../transaction_deletion_record.py | 19 ++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json index bd45b1c109df..aa06d14b170d 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json @@ -57,6 +57,7 @@ "fieldname": "status", "fieldtype": "Select", "label": "Status", + "no_copy": 1, "options": "Queued\nRunning\nFailed\nCompleted\nCancelled", "read_only": 1 }, @@ -118,13 +119,14 @@ "fieldname": "initialize_doctypes_table", "fieldtype": "Check", "label": "Initialize Summary Table", + "no_copy": 1, "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-02-04 15:28:29.532826", + "modified": "2024-02-05 10:25:28.462255", "modified_by": "Administrator", "module": "Setup", "name": "Transaction Deletion Record", diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index 15a9ca9d2cfb..fef5d7f20850 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -61,6 +61,15 @@ def validate_doctypes_to_be_ignored(self): ) def before_submit(self): + if queued_docs := frappe.db.get_all( + "Transaction Deletion Record", filters={"company": self.company, "status": "Queued"} + ): + frappe.throw( + _("There is another document: {0} Queued. Cannot queue multi docs for one company.").format( + self.queued_docs + ) + ) + if not self.doctypes_to_be_ignored: self.populate_doctypes_to_be_ignored_table() @@ -69,6 +78,7 @@ def reset_task_flags(self): self.delete_bin_data = 0 self.delete_leads_and_addresses = 0 self.delete_transactions = 0 + self.initialize_doctypes_table = 0 self.reset_company_default_values = 0 def before_save(self): @@ -91,15 +101,6 @@ def chain_call(self, method): enqueue_after_commit=True, ) - @frappe.whitelist() - def start_deletion_process(self): - self.delete_bins() - self.delete_lead_addresses() - self.reset_company_values() - self.delete_notifications() - self.initialize_doctypes_to_be_deleted_table() - self.delete_company_transactions() - def delete_notifications(self): self.validate_doc_status() if not self.clear_notifications: From e600109872652d3571b3265eb26f35a54498bd28 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 5 Feb 2024 10:42:59 +0530 Subject: [PATCH 20/94] refactor: no copy on summary table and more validations (cherry picked from commit 55e93b3fe14b492e2f648b6ee860e902d87013e6) --- .../transaction_deletion_record.json | 3 ++- .../transaction_deletion_record.py | 14 ++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json index aa06d14b170d..6e057ace4a6e 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json @@ -34,6 +34,7 @@ "fieldname": "doctypes", "fieldtype": "Table", "label": "Summary", + "no_copy": 1, "options": "Transaction Deletion Record Details", "read_only": 1 }, @@ -126,7 +127,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-02-05 10:25:28.462255", + "modified": "2024-02-05 10:36:34.229864", "modified_by": "Administrator", "module": "Setup", "name": "Transaction Deletion Record", diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index fef5d7f20850..87ca9fab5674 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -6,7 +6,7 @@ from frappe import _, qb from frappe.desk.notifications import clear_notifications from frappe.model.document import Document -from frappe.utils import cint, create_batch, get_link_to_form +from frappe.utils import cint, comma_and, create_batch, get_link_to_form class TransactionDeletionRecord(Document): @@ -62,11 +62,16 @@ def validate_doctypes_to_be_ignored(self): def before_submit(self): if queued_docs := frappe.db.get_all( - "Transaction Deletion Record", filters={"company": self.company, "status": "Queued"} + "Transaction Deletion Record", + filters={"company": self.company, "status": ("in", ["Running", "Queued"]), "docstatus": 1}, + pluck="name", ): frappe.throw( - _("There is another document: {0} Queued. Cannot queue multi docs for one company.").format( - self.queued_docs + _( + "Cannot queue multi docs for one company. {0} is already queued/running for company: {1}" + ).format( + comma_and([get_link_to_form("Transaction Deletion Record", x) for x in queued_docs]), + frappe.bold(self.company), ) ) @@ -83,6 +88,7 @@ def reset_task_flags(self): def before_save(self): self.status = "" + self.doctypes.clear() self.reset_task_flags() def on_submit(self): From 962105bc8744fcf24bb1c15ef69a723dc040c417 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 5 Feb 2024 11:52:00 +0530 Subject: [PATCH 21/94] refactor: validations to prevent duplicate jobs (cherry picked from commit 31a2da552b0773f8b7caf510fa06e54cdc1f2e9c) --- .../transaction_deletion_record.py | 75 +++++++++++++++---- 1 file changed, 59 insertions(+), 16 deletions(-) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index 87ca9fab5674..d57f3f609371 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -7,6 +7,7 @@ from frappe.desk.notifications import clear_notifications from frappe.model.document import Document from frappe.utils import cint, comma_and, create_batch, get_link_to_form +from frappe.utils.background_jobs import get_job, is_job_enqueued class TransactionDeletionRecord(Document): @@ -44,6 +45,15 @@ class TransactionDeletionRecord(Document): def __init__(self, *args, **kwargs): super(TransactionDeletionRecord, self).__init__(*args, **kwargs) self.batch_size = 5000 + # Tasks are listged by their execution order + self.task_to_internal_method_map = { + "Delete Bins": "delete_bins", + "Delete Leads and Addresses": "delete_lead_addresses", + "Reset Company Values": "reset_company_values", + "Clear Notifications": "delete_notifications", + "Initialize Summary Table": "initialize_doctypes_to_be_deleted_table", + "Delete Transactions": "delete_company_transactions", + } def validate(self): frappe.only_for("System Manager") @@ -60,6 +70,16 @@ def validate_doctypes_to_be_ignored(self): title=_("Not Allowed"), ) + def generate_job_name_for_task(self, task=None): + method = self.task_to_internal_method_map[task] + return f"{self.name}_{method}" + + def generate_job_name_for_all_tasks(self): + job_names = [] + for method in self.task_to_internal_method_map.values(): + job_names.append(self.generate_job_name_for_task) + return job_names + def before_submit(self): if queued_docs := frappe.db.get_all( "Transaction Deletion Record", @@ -68,7 +88,7 @@ def before_submit(self): ): frappe.throw( _( - "Cannot queue multi docs for one company. {0} is already queued/running for company: {1}" + "Cannot enqueue multi docs for one company. {0} is already queued/running for company: {1}" ).format( comma_and([get_link_to_form("Transaction Deletion Record", x) for x in queued_docs]), frappe.bold(self.company), @@ -97,28 +117,47 @@ def on_submit(self): def on_cancel(self): self.db_set("status", "Cancelled") - def chain_call(self, method): - frappe.enqueue( - "frappe.utils.background_jobs.run_doc_method", - doctype=self.doctype, - name=self.name, - doc_method=method, - queue="long", - enqueue_after_commit=True, - ) + def chain_call(self, task=None): + if task and task in self.task_to_internal_method_map: + method = self.task_to_internal_method_map[task] + job_id = self.generate_job_name_for_task(task) + + frappe.enqueue( + "frappe.utils.background_jobs.run_doc_method", + doctype=self.doctype, + name=self.name, + doc_method=method, + job_id=job_id, + queue="long", + enqueue_after_commit=True, + ) def delete_notifications(self): self.validate_doc_status() if not self.clear_notifications: clear_notifications() self.db_set("clear_notifications", 1) - self.chain_call("initialize_doctypes_to_be_deleted_table") + self.chain_call(task="Initialize Summary Table") def populate_doctypes_to_be_ignored_table(self): doctypes_to_be_ignored_list = get_doctypes_to_be_ignored() for doctype in doctypes_to_be_ignored_list: self.append("doctypes_to_be_ignored", {"doctype_name": doctype}) + def validate_running_task_for_doc(self, job_names: list = None): + # at most only one task should be runnning + running_tasks = [] + for x in job_names: + if is_job_enqueued(x): + running_tasks.append(get_job(x).get_id()) + + if running_tasks: + frappe.throw( + _("{0} is already running for {1}").format( + comma_and([get_link_to_form("RQ Job", x) for x in running_tasks]), self.name + ) + ) + def validate_doc_status(self): if self.status != "Running": frappe.throw( @@ -126,6 +165,9 @@ def validate_doc_status(self): get_link_to_form("Transaction Deletion Record", self.name) ) ) + # make sure that job none of tasks are already running + job_names = self.generate_job_name_for_all_tasks() + self.validate_running_task_for_doc(job_names=job_names) @frappe.whitelist() def delete_bins(self): @@ -139,7 +181,7 @@ def delete_bins(self): self.company, ) self.db_set("delete_bin_data", 1) - self.chain_call(method="delete_lead_addresses") + self.chain_call(task="Delete Leads and Addresses") def delete_lead_addresses(self): """Delete addresses to which leads are linked""" @@ -181,7 +223,7 @@ def delete_lead_addresses(self): ) ) self.db_set("delete_leads_and_addresses", 1) - self.chain_call(method="reset_company_values") + self.chain_call(task="Reset Company Values") def reset_company_values(self): self.validate_doc_status() @@ -191,7 +233,7 @@ def reset_company_values(self): company_obj.sales_monthly_history = None company_obj.save() self.db_set("reset_company_default_values", 1) - self.chain_call(method="delete_notifications") + self.chain_call(task="Clear Notifications") def initialize_doctypes_to_be_deleted_table(self): self.validate_doc_status() @@ -208,7 +250,7 @@ def initialize_doctypes_to_be_deleted_table(self): # Initialize self.populate_doctypes_table(tables, docfield["parent"], docfield["fieldname"], 0) self.db_set("initialize_doctypes_table", 1) - self.chain_call(method="delete_company_transactions") + self.chain_call(task="Delete Transactions") def delete_company_transactions(self): self.validate_doc_status() @@ -248,7 +290,8 @@ def delete_company_transactions(self): docfield.doctype, filters={"parent": self.name, "done": 0}, pluck="doctype_name" ) if pending_doctypes: - self.chain_call(method="delete_company_transactions") + # as method is enqueued after commit, calling itself will not make validate_doc_status to throw + self.chain_call(task="Delete Transactions") else: self.db_set("status", "Completed") self.db_set("delete_transactions", 1) From e142daca7fcd7b5904ea144ffa21d593f5da337f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 5 Feb 2024 17:35:14 +0530 Subject: [PATCH 22/94] chore: hide docfield in list view (cherry picked from commit 98afb4d468145ac2270362841468f2ccb05b501f) --- .../transaction_deletion_record_details.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/transaction_deletion_record_details/transaction_deletion_record_details.json b/erpnext/accounts/doctype/transaction_deletion_record_details/transaction_deletion_record_details.json index e8a5eb6c4321..fe4b0852ac1b 100644 --- a/erpnext/accounts/doctype/transaction_deletion_record_details/transaction_deletion_record_details.json +++ b/erpnext/accounts/doctype/transaction_deletion_record_details/transaction_deletion_record_details.json @@ -24,7 +24,6 @@ { "fieldname": "docfield_name", "fieldtype": "Data", - "in_list_view": 1, "label": "DocField", "read_only": 1 }, @@ -47,7 +46,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-02-04 10:55:52.060417", + "modified": "2024-02-05 17:35:09.556054", "modified_by": "Administrator", "module": "Accounts", "name": "Transaction Deletion Record Details", From e9080033585003d07a70362c1ce71b1bb0a8882a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 5 Feb 2024 20:35:29 +0530 Subject: [PATCH 23/94] refactor: make sure only one task is running for doc (cherry picked from commit 78c9cc63b1011761dd3fed84edb5c0b41ff9f3f5) --- .../transaction_deletion_record.js | 2 +- .../transaction_deletion_record.py | 48 ++++++++++++------- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js index ed70ebb5f700..ccf09a6c38b0 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js @@ -24,7 +24,7 @@ frappe.ui.form.on("Transaction Deletion Record", { frm.add_custom_button(execute_btn, () => { // Entry point for chain of events frm.call({ - method: 'delete_bins', + method: 'process_tasks', doc: frm.doc }); }); diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index d57f3f609371..0ae587e1f868 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -1,6 +1,7 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +from collections import OrderedDict import frappe from frappe import _, qb @@ -45,15 +46,17 @@ class TransactionDeletionRecord(Document): def __init__(self, *args, **kwargs): super(TransactionDeletionRecord, self).__init__(*args, **kwargs) self.batch_size = 5000 - # Tasks are listged by their execution order - self.task_to_internal_method_map = { - "Delete Bins": "delete_bins", - "Delete Leads and Addresses": "delete_lead_addresses", - "Reset Company Values": "reset_company_values", - "Clear Notifications": "delete_notifications", - "Initialize Summary Table": "initialize_doctypes_to_be_deleted_table", - "Delete Transactions": "delete_company_transactions", - } + # Tasks are listed by their execution order + self.task_to_internal_method_map = OrderedDict( + { + "Delete Bins": "delete_bins", + "Delete Leads and Addresses": "delete_lead_addresses", + "Reset Company Values": "reset_company_values", + "Clear Notifications": "delete_notifications", + "Initialize Summary Table": "initialize_doctypes_to_be_deleted_table", + "Delete Transactions": "delete_company_transactions", + } + ) def validate(self): frappe.only_for("System Manager") @@ -74,10 +77,19 @@ def generate_job_name_for_task(self, task=None): method = self.task_to_internal_method_map[task] return f"{self.name}_{method}" + def generate_job_name_for_next_tasks(self, task=None): + job_names = [] + current_task_idx = list(self.task_to_internal_method_map).index(task) + for idx, task in enumerate(self.task_to_internal_method_map.keys(), 0): + # generate job_name for next tasks + if idx > current_task_idx: + job_names.append(self.generate_job_name_for_task(task)) + return job_names + def generate_job_name_for_all_tasks(self): job_names = [] - for method in self.task_to_internal_method_map.values(): - job_names.append(self.generate_job_name_for_task) + for task in self.task_to_internal_method_map.keys(): + job_names.append(self.generate_job_name_for_task(task)) return job_names def before_submit(self): @@ -119,6 +131,10 @@ def on_cancel(self): def chain_call(self, task=None): if task and task in self.task_to_internal_method_map: + # make sure that none of next tasks are already running + job_names = self.generate_job_name_for_next_tasks(task=task) + self.validate_running_task_for_doc(job_names=job_names) + method = self.task_to_internal_method_map[task] job_id = self.generate_job_name_for_task(task) @@ -165,15 +181,15 @@ def validate_doc_status(self): get_link_to_form("Transaction Deletion Record", self.name) ) ) - # make sure that job none of tasks are already running - job_names = self.generate_job_name_for_all_tasks() - self.validate_running_task_for_doc(job_names=job_names) @frappe.whitelist() - def delete_bins(self): - # This methid is the entry point for the chain of events that follow + def process_tasks(self): + # This method is the entry point for the chain of events that follow self.db_set("status", "Running") + self.chain_call(task="Delete Bins") + def delete_bins(self): + self.validate_doc_status() if not self.delete_bin_data: frappe.db.sql( """delete from `tabBin` where warehouse in From cf6cb80d180a9fbd12e422503b759302b1ad8be8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 7 Feb 2024 20:54:29 +0530 Subject: [PATCH 24/94] refactor: barebones hook on all doctypes with 'company' field (cherry picked from commit ec194ef076037b16edea68728069b3308a9a4216) --- erpnext/hooks.py | 5 ++++- .../transaction_deletion_record.py | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 5a4e8539bf6b..e328c686f5dc 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -299,7 +299,10 @@ doc_events = { "*": { - "validate": "erpnext.support.doctype.service_level_agreement.service_level_agreement.apply", + "validate": [ + "erpnext.support.doctype.service_level_agreement.service_level_agreement.apply", + "erpnext.setup.doctype.transaction_deletion_record.transaction_deletion_record.check_for_running_deletion_job", + ], }, tuple(period_closing_doctypes): { "validate": "erpnext.accounts.doctype.accounting_period.accounting_period.validate_accounting_period_on_doc_save", diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index 0ae587e1f868..4d6463ca5e53 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -453,3 +453,24 @@ def get_doctypes_to_be_ignored(): doctypes_to_be_ignored.extend(frappe.get_hooks("company_data_to_be_ignored") or []) return doctypes_to_be_ignored + + +def check_for_running_deletion_job(doc, method=None): + df = qb.DocType("DocField") + if ( + not_allowed := qb.from_(df) + .select(df.parent) + .where((df.fieldname == "company") & (df.parent == doc.doctype)) + .run() + ): + if running_deletion_jobs := frappe.db.get_all( + "Transaction Deletion Record", + filters={"docstatus": 1, "company": doc.company, "status": "Running"}, + ): + frappe.throw( + _( + "Transaction Deletion job {0} is running for this Company. Cannot make any transactions until the deletion job is completed" + ).format( + get_link_to_form("Transaction Deletion Record", running_deletion_jobs[0].name) + ) + ) From 0ea9ce7a5aa1716533218662c1f7d62d14169ec0 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 8 Feb 2024 16:08:59 +0530 Subject: [PATCH 25/94] refactor: better method naming (cherry picked from commit 30463657bf7f44321b690a79c94e24d6286d486e) --- .../transaction_deletion_record.py | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index 4d6463ca5e53..bc30c3f505c2 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -129,31 +129,40 @@ def on_submit(self): def on_cancel(self): self.db_set("status", "Cancelled") - def chain_call(self, task=None): + def enqueue_task(self, task: str | None = None): if task and task in self.task_to_internal_method_map: # make sure that none of next tasks are already running job_names = self.generate_job_name_for_next_tasks(task=task) self.validate_running_task_for_doc(job_names=job_names) - method = self.task_to_internal_method_map[task] + # method = self.task_to_internal_method_map[task] + # Generate Job Id to uniquely identify each task for this document job_id = self.generate_job_name_for_task(task) frappe.enqueue( "frappe.utils.background_jobs.run_doc_method", doctype=self.doctype, name=self.name, - doc_method=method, + doc_method="execute_task", job_id=job_id, queue="long", enqueue_after_commit=True, + task_to_execute=task, ) + def execute_task(self, task_to_execute: str | None = None): + if task_to_execute: + pass + method = self.task_to_internal_method_map[task_to_execute] + if task := getattr(self, method, None): + task() + def delete_notifications(self): self.validate_doc_status() if not self.clear_notifications: clear_notifications() self.db_set("clear_notifications", 1) - self.chain_call(task="Initialize Summary Table") + self.enqueue_task(task="Initialize Summary Table") def populate_doctypes_to_be_ignored_table(self): doctypes_to_be_ignored_list = get_doctypes_to_be_ignored() @@ -186,7 +195,7 @@ def validate_doc_status(self): def process_tasks(self): # This method is the entry point for the chain of events that follow self.db_set("status", "Running") - self.chain_call(task="Delete Bins") + self.enqueue_task(task="Delete Bins") def delete_bins(self): self.validate_doc_status() @@ -197,7 +206,7 @@ def delete_bins(self): self.company, ) self.db_set("delete_bin_data", 1) - self.chain_call(task="Delete Leads and Addresses") + self.enqueue_task(task="Delete Leads and Addresses") def delete_lead_addresses(self): """Delete addresses to which leads are linked""" @@ -239,7 +248,7 @@ def delete_lead_addresses(self): ) ) self.db_set("delete_leads_and_addresses", 1) - self.chain_call(task="Reset Company Values") + self.enqueue_task(task="Reset Company Values") def reset_company_values(self): self.validate_doc_status() @@ -249,7 +258,7 @@ def reset_company_values(self): company_obj.sales_monthly_history = None company_obj.save() self.db_set("reset_company_default_values", 1) - self.chain_call(task="Clear Notifications") + self.enqueue_task(task="Clear Notifications") def initialize_doctypes_to_be_deleted_table(self): self.validate_doc_status() @@ -266,7 +275,7 @@ def initialize_doctypes_to_be_deleted_table(self): # Initialize self.populate_doctypes_table(tables, docfield["parent"], docfield["fieldname"], 0) self.db_set("initialize_doctypes_table", 1) - self.chain_call(task="Delete Transactions") + self.enqueue_task(task="Delete Transactions") def delete_company_transactions(self): self.validate_doc_status() @@ -307,7 +316,8 @@ def delete_company_transactions(self): ) if pending_doctypes: # as method is enqueued after commit, calling itself will not make validate_doc_status to throw - self.chain_call(task="Delete Transactions") + # recursively call this task to delete all transactions + self.enqueue_task(task="Delete Transactions") else: self.db_set("status", "Completed") self.db_set("delete_transactions", 1) From 1110dd93ff06cffc0f5e537bb552356aa1dd3bb4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 19 Mar 2024 11:18:11 +0530 Subject: [PATCH 26/94] chore: code cleanup (cherry picked from commit eea260b9f9c4a4a3a5a716b1d74a45c7569d4f98) --- .../transaction_deletion_record.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index bc30c3f505c2..d66158e9e50b 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -135,7 +135,6 @@ def enqueue_task(self, task: str | None = None): job_names = self.generate_job_name_for_next_tasks(task=task) self.validate_running_task_for_doc(job_names=job_names) - # method = self.task_to_internal_method_map[task] # Generate Job Id to uniquely identify each task for this document job_id = self.generate_job_name_for_task(task) @@ -152,7 +151,6 @@ def enqueue_task(self, task: str | None = None): def execute_task(self, task_to_execute: str | None = None): if task_to_execute: - pass method = self.task_to_internal_method_map[task_to_execute] if task := getattr(self, method, None): task() @@ -312,7 +310,9 @@ def delete_company_transactions(self): frappe.db.set_value(docfield.doctype, docfield.name, "done", 1) pending_doctypes = frappe.db.get_all( - docfield.doctype, filters={"parent": self.name, "done": 0}, pluck="doctype_name" + "Transaction Deletion Record Details", + filters={"parent": self.name, "done": 0}, + pluck="doctype_name", ) if pending_doctypes: # as method is enqueued after commit, calling itself will not make validate_doc_status to throw From 5e590389f37f34df5b7811661acc30cb4304b788 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 19 Mar 2024 17:39:20 +0530 Subject: [PATCH 27/94] refactor: exception propogation (cherry picked from commit 4a55240e630fedf1590a2c324528fd5934066d37) --- .../transaction_deletion_record.json | 9 ++++++++- .../transaction_deletion_record.py | 13 ++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json index 6e057ace4a6e..8291a4ffc246 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json @@ -10,6 +10,7 @@ "column_break_txbg", "status", "tasks_section", + "error_log", "delete_bin_data", "delete_leads_and_addresses", "reset_company_default_values", @@ -122,12 +123,18 @@ "label": "Initialize Summary Table", "no_copy": 1, "read_only": 1 + }, + { + "depends_on": "eval: doc.error_log", + "fieldname": "error_log", + "fieldtype": "Long Text", + "label": "Error Log" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-02-05 10:36:34.229864", + "modified": "2024-03-19 17:04:49.369734", "modified_by": "Administrator", "module": "Setup", "name": "Transaction Deletion Record", diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index d66158e9e50b..af95ce69a082 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -37,6 +37,7 @@ class TransactionDeletionRecord(Document): delete_transactions: DF.Check doctypes: DF.Table[TransactionDeletionRecordDetails] doctypes_to_be_ignored: DF.Table[TransactionDeletionRecordItem] + error_log: DF.LongText | None initialize_doctypes_table: DF.Check reset_company_default_values: DF.Check status: DF.Literal["Queued", "Running", "Failed", "Completed", "Cancelled"] @@ -149,11 +150,21 @@ def enqueue_task(self, task: str | None = None): task_to_execute=task, ) + # todo: add a non-background job based approach as well + def execute_task(self, task_to_execute: str | None = None): if task_to_execute: method = self.task_to_internal_method_map[task_to_execute] if task := getattr(self, method, None): - task() + try: + task() + except Exception as err: + frappe.db.rollback() + traceback = frappe.get_traceback(with_context=True) + if traceback: + message = "Traceback:
" + traceback + frappe.db.set_value(self.doctype, self.name, "error_log", message) + frappe.db.set_value(self.doctype, self.name, "status", "Failed") def delete_notifications(self): self.validate_doc_status() From 6ae9eb250f5a5578507c4e0a6ddd50d109b752cd Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 19 Mar 2024 17:41:37 +0530 Subject: [PATCH 28/94] refactor: minor UI tweaks (cherry picked from commit 0455d0c46c8a171c0bbbd6f1730f671263cfedc4) --- .../transaction_deletion_record.js | 16 +++++++--------- .../transaction_deletion_record.py | 1 + 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js index ccf09a6c38b0..d543baa3cf60 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js @@ -10,26 +10,24 @@ frappe.ui.form.on("Transaction Deletion Record", { callback: function (r) { doctypes_to_be_ignored_array = r.message; populate_doctypes_to_be_ignored(doctypes_to_be_ignored_array, frm); - frm.refresh_field('doctypes_to_be_ignored'); - } + frm.refresh_field("doctypes_to_be_ignored"); + }, }); } - }, - refresh: function(frm) { - if (frm.doc.docstatus==1 && ['Queued', 'Failed'].find(x => x == frm.doc.status)) { - let execute_btn = __("Start Deletion") + refresh: function (frm) { + if (frm.doc.docstatus == 1 && ["Queued", "Failed"].find((x) => x == frm.doc.status)) { + let execute_btn = frm.doc.status == "Queued" ? __("Start Deletion") : __("Retry"); frm.add_custom_button(execute_btn, () => { // Entry point for chain of events frm.call({ - method: 'process_tasks', - doc: frm.doc + method: "process_tasks", + doc: frm.doc, }); }); } - }, }); diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index af95ce69a082..c2ea56a96ceb 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -332,6 +332,7 @@ def delete_company_transactions(self): else: self.db_set("status", "Completed") self.db_set("delete_transactions", 1) + self.db_set("error_log", None) def get_doctypes_to_be_ignored_list(self): singles = frappe.get_all("DocType", filters={"issingle": 1}, pluck="name") From d496a1e58e17edf6b0c6b18bb9e3b06b94e024d2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 19 Mar 2024 17:49:05 +0530 Subject: [PATCH 29/94] chore: move status and error log to their own section (cherry picked from commit 3cec62d4f872a66557e1b9e56a2207f194e5439f) --- .../transaction_deletion_record.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json index 8291a4ffc246..688b14a808ab 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json @@ -7,10 +7,10 @@ "engine": "InnoDB", "field_order": [ "company", - "column_break_txbg", + "section_break_qpwb", "status", - "tasks_section", "error_log", + "tasks_section", "delete_bin_data", "delete_leads_and_addresses", "reset_company_default_values", @@ -63,10 +63,6 @@ "options": "Queued\nRunning\nFailed\nCompleted\nCancelled", "read_only": 1 }, - { - "fieldname": "column_break_txbg", - "fieldtype": "Column Break" - }, { "fieldname": "section_break_tbej", "fieldtype": "Section Break" @@ -129,12 +125,16 @@ "fieldname": "error_log", "fieldtype": "Long Text", "label": "Error Log" + }, + { + "fieldname": "section_break_qpwb", + "fieldtype": "Section Break" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-03-19 17:04:49.369734", + "modified": "2024-03-19 17:47:04.490196", "modified_by": "Administrator", "module": "Setup", "name": "Transaction Deletion Record", From fab5c1170d4383d7267bf2fbee0f740c616df5e5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 20 Mar 2024 14:10:08 +0530 Subject: [PATCH 30/94] chore: rename entry point (cherry picked from commit 5fe0b20be108ce55a9519af5c953f7aebeaca93b) --- .../transaction_deletion_record/transaction_deletion_record.js | 2 +- .../transaction_deletion_record/transaction_deletion_record.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js index d543baa3cf60..9aa027841659 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.js @@ -23,7 +23,7 @@ frappe.ui.form.on("Transaction Deletion Record", { frm.add_custom_button(execute_btn, () => { // Entry point for chain of events frm.call({ - method: "process_tasks", + method: "start_deletion_tasks", doc: frm.doc, }); }); diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index c2ea56a96ceb..5d4f95f5b611 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -201,7 +201,7 @@ def validate_doc_status(self): ) @frappe.whitelist() - def process_tasks(self): + def start_deletion_tasks(self): # This method is the entry point for the chain of events that follow self.db_set("status", "Running") self.enqueue_task(task="Delete Bins") From b429f75d65ca001a6db29b95ff70cf8926d97452 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 20 Mar 2024 14:32:27 +0530 Subject: [PATCH 31/94] refactor: link running doc validation to company master (cherry picked from commit 5a3afea8c772b7167839593ba88f3582a381259c) # Conflicts: # erpnext/setup/doctype/company/company.js # erpnext/setup/doctype/company/company.py --- erpnext/setup/doctype/company/company.js | 43 +++++++++++++++++++ erpnext/setup/doctype/company/company.py | 23 ++++++++++ .../transaction_deletion_record.py | 32 +++++++++----- 3 files changed, 87 insertions(+), 11 deletions(-) diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 23a55487adce..39aca3506509 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -169,6 +169,7 @@ frappe.ui.form.on("Company", { }, delete_company_transactions: function (frm) { +<<<<<<< HEAD frappe.verify_password(function () { var d = frappe.prompt( { @@ -200,6 +201,48 @@ frappe.ui.form.on("Company", { onerror: function () { frappe.msgprint(__("Wrong Password")); }, +======= + frappe.call({ + method: "erpnext.setup.doctype.transaction_deletion_record.transaction_deletion_record.is_deletion_doc_running", + args: { + company: frm.doc.name, + }, + freeze: true, + callback: function (r) { + if (!r.exc) { + frappe.verify_password(function () { + var d = frappe.prompt( + { + fieldtype: "Data", + fieldname: "company_name", + label: __("Please enter the company name to confirm"), + reqd: 1, + description: __( + "Please make sure you really want to delete all the transactions for this company. Your master data will remain as it is. This action cannot be undone." + ), + }, + function (data) { + if (data.company_name !== frm.doc.name) { + frappe.msgprint(__("Company name not same")); + return; + } + frappe.call({ + method: "erpnext.setup.doctype.company.company.create_transaction_deletion_request", + args: { + company: data.company_name, + }, + freeze: true, + callback: function (r, rt) {}, + onerror: function () { + frappe.msgprint(__("Wrong Password")); + }, + }); + }, + __("Delete all the Transactions for this Company"), + __("Delete") + ); + d.get_primary_btn().addClass("btn-danger"); +>>>>>>> 5a3afea8c7 (refactor: link running doc validation to company master) }); }, __("Delete all the Transactions for this Company"), diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index bea76f19412b..7d861979daa8 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -11,7 +11,11 @@ from frappe.contacts.address_and_contact import load_address_and_contact from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.desk.page.setup_wizard.setup_wizard import make_records +<<<<<<< HEAD from frappe.utils import cint, formatdate, get_timestamp, today +======= +from frappe.utils import cint, formatdate, get_link_to_form, get_timestamp, today +>>>>>>> 5a3afea8c7 (refactor: link running doc validation to company master) from frappe.utils.nestedset import NestedSet, rebuild_tree from erpnext.accounts.doctype.account.account import get_account_currency @@ -812,6 +816,25 @@ def get_default_company_address(name, sort_key="is_primary_address", existing_ad @frappe.whitelist() def create_transaction_deletion_request(company): +<<<<<<< HEAD tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company}) tdr.insert() tdr.submit() +======= + from erpnext.setup.doctype.transaction_deletion_record.transaction_deletion_record import ( + is_deletion_doc_running, + ) + + is_deletion_doc_running(company) + + tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company}) + tdr.submit() + tdr.start_deletion_tasks() + + frappe.msgprint( + _("A Transaction Deletion Document: {0} is triggered for {0}").format( + get_link_to_form("Transaction Deletion Record", tdr.name) + ), + frappe.bold(company), + ) +>>>>>>> 5a3afea8c7 (refactor: link running doc validation to company master) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index 5d4f95f5b611..2140b519f4cb 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -477,7 +477,25 @@ def get_doctypes_to_be_ignored(): return doctypes_to_be_ignored +@frappe.whitelist() +def is_deletion_doc_running(company: str | None = None, err_msg: str | None = None): + if company: + if running_deletion_jobs := frappe.db.get_all( + "Transaction Deletion Record", + filters={"docstatus": 1, "company": company, "status": "Running"}, + ): + if not err_msg: + err_msg = "" + frappe.throw( + title=_("Deletion in Progress!"), + msg=_("Transaction Deletion Document: {0} is running for this Company. {1}").format( + get_link_to_form("Transaction Deletion Record", running_deletion_jobs[0].name), err_msg + ), + ) + + def check_for_running_deletion_job(doc, method=None): + # Check if DocType has 'company' field df = qb.DocType("DocField") if ( not_allowed := qb.from_(df) @@ -485,14 +503,6 @@ def check_for_running_deletion_job(doc, method=None): .where((df.fieldname == "company") & (df.parent == doc.doctype)) .run() ): - if running_deletion_jobs := frappe.db.get_all( - "Transaction Deletion Record", - filters={"docstatus": 1, "company": doc.company, "status": "Running"}, - ): - frappe.throw( - _( - "Transaction Deletion job {0} is running for this Company. Cannot make any transactions until the deletion job is completed" - ).format( - get_link_to_form("Transaction Deletion Record", running_deletion_jobs[0].name) - ) - ) + is_deletion_doc_running( + doc.company, _("Cannot make any transactions until the deletion job is completed") + ) From 1fe14334f33465cc2e2e4eb188e6dad1aa434f2e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 20 Mar 2024 14:59:19 +0530 Subject: [PATCH 32/94] refactor: ability to process in single transaction (cherry picked from commit a158b825d7eb359a66743b3e6972aa1b81389df0) --- .../transaction_deletion_record.json | 11 ++++++-- .../transaction_deletion_record.py | 26 ++++++++++--------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json index 688b14a808ab..e03e1695e0ef 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json @@ -20,7 +20,8 @@ "section_break_tbej", "doctypes", "doctypes_to_be_ignored", - "amended_from" + "amended_from", + "process_in_single_transaction" ], "fields": [ { @@ -129,12 +130,18 @@ { "fieldname": "section_break_qpwb", "fieldtype": "Section Break" + }, + { + "default": "0", + "fieldname": "process_in_single_transaction", + "fieldtype": "Check", + "label": "Process in Single Transaction" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-03-19 17:47:04.490196", + "modified": "2024-03-20 14:58:15.086360", "modified_by": "Administrator", "module": "Setup", "name": "Transaction Deletion Record", diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index 2140b519f4cb..a144525fe13a 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -39,6 +39,7 @@ class TransactionDeletionRecord(Document): doctypes_to_be_ignored: DF.Table[TransactionDeletionRecordItem] error_log: DF.LongText | None initialize_doctypes_table: DF.Check + process_in_single_transaction: DF.Check reset_company_default_values: DF.Check status: DF.Literal["Queued", "Running", "Failed", "Completed", "Cancelled"] # end: auto-generated types @@ -139,18 +140,19 @@ def enqueue_task(self, task: str | None = None): # Generate Job Id to uniquely identify each task for this document job_id = self.generate_job_name_for_task(task) - frappe.enqueue( - "frappe.utils.background_jobs.run_doc_method", - doctype=self.doctype, - name=self.name, - doc_method="execute_task", - job_id=job_id, - queue="long", - enqueue_after_commit=True, - task_to_execute=task, - ) - - # todo: add a non-background job based approach as well + if self.process_in_single_transaction: + self.execute_task(task_to_execute=task) + else: + frappe.enqueue( + "frappe.utils.background_jobs.run_doc_method", + doctype=self.doctype, + name=self.name, + doc_method="execute_task", + job_id=job_id, + queue="long", + enqueue_after_commit=True, + task_to_execute=task, + ) def execute_task(self, task_to_execute: str | None = None): if task_to_execute: From 35fcd032ad334e5efb0fce41373e6c59db9a54ed Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 20 Mar 2024 15:16:33 +0530 Subject: [PATCH 33/94] refactor(test): test cases modified to handle new approach (cherry picked from commit 81309576b0cb41ac2c91cf1abbf79b4655c7697d) # Conflicts: # erpnext/setup/demo.py # erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py --- erpnext/setup/demo.py | 228 ++++++++++++++++++ .../test_transaction_deletion_record.py | 7 + 2 files changed, 235 insertions(+) create mode 100644 erpnext/setup/demo.py diff --git a/erpnext/setup/demo.py b/erpnext/setup/demo.py new file mode 100644 index 000000000000..f48402e175b4 --- /dev/null +++ b/erpnext/setup/demo.py @@ -0,0 +1,228 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import json +import os +from random import randint + +import frappe +from frappe import _ +from frappe.utils import add_days, getdate + +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.accounts.utils import get_fiscal_year +from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice +from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice +from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account + + +def setup_demo_data(): + from frappe.utils.telemetry import capture + + capture("demo_data_creation_started", "erpnext") + try: + company = create_demo_company() + process_masters() + make_transactions(company) + frappe.cache.delete_keys("bootinfo") + frappe.publish_realtime("demo_data_complete") + except Exception: + frappe.log_error("Failed to create demo data") + capture("demo_data_creation_failed", "erpnext", properties={"exception": frappe.get_traceback()}) + raise + capture("demo_data_creation_completed", "erpnext") + + +@frappe.whitelist() +def clear_demo_data(): + from frappe.utils.telemetry import capture + + frappe.only_for("System Manager") + + capture("demo_data_erased", "erpnext") + try: + company = frappe.db.get_single_value("Global Defaults", "demo_company") + create_transaction_deletion_record(company) + clear_masters() + delete_company(company) + default_company = frappe.db.get_single_value("Global Defaults", "default_company") + frappe.db.set_default("company", default_company) + except Exception: + frappe.db.rollback() + frappe.log_error("Failed to erase demo data") + frappe.throw( + _("Failed to erase demo data, please delete the demo company manually."), + title=_("Could Not Delete Demo Data"), + ) + + +def create_demo_company(): + company = frappe.db.get_all("Company")[0].name + company_doc = frappe.get_doc("Company", company) + + # Make a dummy company + new_company = frappe.new_doc("Company") + new_company.company_name = company_doc.company_name + " (Demo)" + new_company.abbr = company_doc.abbr + "D" + new_company.enable_perpetual_inventory = 1 + new_company.default_currency = company_doc.default_currency + new_company.country = company_doc.country + new_company.chart_of_accounts_based_on = "Standard Template" + new_company.chart_of_accounts = company_doc.chart_of_accounts + new_company.insert() + + # Set Demo Company as default to + frappe.db.set_single_value("Global Defaults", "demo_company", new_company.name) + frappe.db.set_default("company", new_company.name) + + bank_account = create_bank_account({"company_name": new_company.name}) + frappe.db.set_value("Company", new_company.name, "default_bank_account", bank_account.name) + + return new_company.name + + +def process_masters(): + for doctype in frappe.get_hooks("demo_master_doctypes"): + data = read_data_file_using_hooks(doctype) + if data: + for item in json.loads(data): + create_demo_record(item) + + +def create_demo_record(doctype): + frappe.get_doc(doctype).insert(ignore_permissions=True) + + +def make_transactions(company): + frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1) + from erpnext.accounts.utils import FiscalYearError + + try: + start_date = get_fiscal_year(date=getdate())[1] + except FiscalYearError: + # User might have setup fiscal year for previous or upcoming years + active_fiscal_years = frappe.db.get_all("Fiscal Year", filters={"disabled": 0}, as_list=1) + if active_fiscal_years: + start_date = frappe.db.get_value("Fiscal Year", active_fiscal_years[0][0], "year_start_date") + else: + frappe.throw(_("There are no active Fiscal Years for which Demo Data can be generated.")) + + for doctype in frappe.get_hooks("demo_transaction_doctypes"): + data = read_data_file_using_hooks(doctype) + if data: + for item in json.loads(data): + create_transaction(item, company, start_date) + + convert_order_to_invoices() + frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 0) + + +def create_transaction(doctype, company, start_date): + document_type = doctype.get("doctype") + warehouse = get_warehouse(company) + + if document_type == "Purchase Order": + posting_date = get_random_date(start_date, 1, 25) + else: + posting_date = get_random_date(start_date, 31, 350) + + doctype.update( + { + "company": company, + "set_posting_time": 1, + "transaction_date": posting_date, + "schedule_date": posting_date, + "delivery_date": posting_date, + "set_warehouse": warehouse, + } + ) + + doc = frappe.get_doc(doctype) + doc.save(ignore_permissions=True) + doc.submit() + + +def convert_order_to_invoices(): + for document in ["Purchase Order", "Sales Order"]: + # Keep some orders intentionally unbilled/unpaid + for i, order in enumerate( + frappe.db.get_all( + document, filters={"docstatus": 1}, fields=["name", "transaction_date"], limit=6 + ) + ): + + if document == "Purchase Order": + invoice = make_purchase_invoice(order.name) + elif document == "Sales Order": + invoice = make_sales_invoice(order.name) + + invoice.set_posting_time = 1 + invoice.posting_date = order.transaction_date + invoice.due_date = order.transaction_date + invoice.bill_date = order.transaction_date + + if invoice.get("payment_schedule"): + invoice.payment_schedule[0].due_date = order.transaction_date + + invoice.update_stock = 1 + invoice.submit() + + if i % 2 != 0: + payment = get_payment_entry(invoice.doctype, invoice.name) + payment.posting_date = order.transaction_date + payment.reference_no = invoice.name + payment.submit() + + +def get_random_date(start_date, start_range, end_range): + return add_days(start_date, randint(start_range, end_range)) + + +def create_transaction_deletion_record(company): + transaction_deletion_record = frappe.new_doc("Transaction Deletion Record") + transaction_deletion_record.company = company + transaction_deletion_record.process_in_single_transaction = True + transaction_deletion_record.save(ignore_permissions=True) + transaction_deletion_record.submit() + transaction_deletion_record.start_deletion_tasks() + + +def clear_masters(): + for doctype in frappe.get_hooks("demo_master_doctypes")[::-1]: + data = read_data_file_using_hooks(doctype) + if data: + for item in json.loads(data): + clear_demo_record(item) + + +def clear_demo_record(document): + document_type = document.get("doctype") + del document["doctype"] + + valid_columns = frappe.get_meta(document_type).get_valid_columns() + + filters = document + for key in list(filters): + if key not in valid_columns: + filters.pop(key, None) + + doc = frappe.get_doc(document_type, filters) + doc.delete(ignore_permissions=True) + + +def delete_company(company): + frappe.db.set_single_value("Global Defaults", "demo_company", "") + frappe.delete_doc("Company", company, ignore_permissions=True) + + +def read_data_file_using_hooks(doctype): + path = os.path.join(os.path.dirname(__file__), "demo_data") + with open(os.path.join(path, doctype + ".json"), "r") as f: + data = f.read() + + return data + + +def get_warehouse(company): + warehouses = frappe.db.get_all("Warehouse", {"company": company, "is_group": 0}) + return warehouses[randint(0, 3)].name diff --git a/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py index 319d435ca69f..78d6ea67bff5 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py @@ -27,7 +27,12 @@ def test_doctypes_contain_company_field(self): def test_no_of_docs_is_correct(self): for i in range(5): create_task("Dunder Mifflin Paper Co") +<<<<<<< HEAD tdr = create_transaction_deletion_request("Dunder Mifflin Paper Co") +======= + tdr = create_transaction_deletion_doc("Dunder Mifflin Paper Co") + tdr.reload() +>>>>>>> 81309576b0 (refactor(test): test cases modified to handle new approach) for doctype in tdr.doctypes: if doctype.doctype_name == "Task": self.assertEqual(doctype.no_of_docs, 5) @@ -49,7 +54,9 @@ def create_company(company_name): def create_transaction_deletion_request(company): tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company}) tdr.insert() + tdr.process_in_single_transaction = True tdr.submit() + tdr.start_deletion_tasks() return tdr From e5722a772a7cbdb17bc9023555a574c85637ebe6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 20 Mar 2024 16:01:09 +0530 Subject: [PATCH 34/94] chore: fix linting issue in JS (cherry picked from commit 02c522b7cddf0b1ae4bcc1d05e156a5b7aa09f2f) --- .../transaction_deletion_record_list.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record_list.js b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record_list.js index 7c7b8ff25a72..285cb6dd2211 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record_list.js +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record_list.js @@ -1,16 +1,16 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.listview_settings['Transaction Deletion Record'] = { +frappe.listview_settings["Transaction Deletion Record"] = { add_fields: ["status"], - get_indicator: function(doc) { + get_indicator: function (doc) { let colors = { - 'Queued': 'orange', - 'Completed': 'green', - 'Running': 'blue', - 'Failed': 'red', + Queued: "orange", + Completed: "green", + Running: "blue", + Failed: "red", }; let status = doc.status; - return [__(status), colors[status], 'status,=,'+status]; + return [__(status), colors[status], "status,=," + status]; }, }; From 87fdb4e7209d34dfa844c0a59cdaf39b594b6996 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 20 Mar 2024 19:48:45 +0530 Subject: [PATCH 35/94] chore: resolve conflicts --- erpnext/setup/doctype/company/company.js | 41 +------------------ erpnext/setup/doctype/company/company.py | 10 ----- .../test_transaction_deletion_record.py | 4 -- .../transaction_deletion_record.py | 33 --------------- .../transaction_deletion_record_item.py | 17 -------- 5 files changed, 2 insertions(+), 103 deletions(-) diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 39aca3506509..6c237d787bb0 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -169,39 +169,6 @@ frappe.ui.form.on("Company", { }, delete_company_transactions: function (frm) { -<<<<<<< HEAD - frappe.verify_password(function () { - var d = frappe.prompt( - { - fieldtype: "Data", - fieldname: "company_name", - label: __("Please enter the company name to confirm"), - reqd: 1, - description: __( - "Please make sure you really want to delete all the transactions for this company. Your master data will remain as it is. This action cannot be undone." - ), - }, - function (data) { - if (data.company_name !== frm.doc.name) { - frappe.msgprint(__("Company name not same")); - return; - } - frappe.call({ - method: "erpnext.setup.doctype.company.company.create_transaction_deletion_request", - args: { - company: data.company_name, - }, - freeze: true, - callback: function (r, rt) { - if (!r.exc) - frappe.msgprint( - __("Successfully deleted all transactions related to this company!") - ); - }, - onerror: function () { - frappe.msgprint(__("Wrong Password")); - }, -======= frappe.call({ method: "erpnext.setup.doctype.transaction_deletion_record.transaction_deletion_record.is_deletion_doc_running", args: { @@ -242,13 +209,9 @@ frappe.ui.form.on("Company", { __("Delete") ); d.get_primary_btn().addClass("btn-danger"); ->>>>>>> 5a3afea8c7 (refactor: link running doc validation to company master) }); - }, - __("Delete all the Transactions for this Company"), - __("Delete") - ); - d.get_primary_btn().addClass("btn-danger"); + } + }, }); }, }); diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 7d861979daa8..5ee7dbb4b47e 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -11,11 +11,7 @@ from frappe.contacts.address_and_contact import load_address_and_contact from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.desk.page.setup_wizard.setup_wizard import make_records -<<<<<<< HEAD -from frappe.utils import cint, formatdate, get_timestamp, today -======= from frappe.utils import cint, formatdate, get_link_to_form, get_timestamp, today ->>>>>>> 5a3afea8c7 (refactor: link running doc validation to company master) from frappe.utils.nestedset import NestedSet, rebuild_tree from erpnext.accounts.doctype.account.account import get_account_currency @@ -816,11 +812,6 @@ def get_default_company_address(name, sort_key="is_primary_address", existing_ad @frappe.whitelist() def create_transaction_deletion_request(company): -<<<<<<< HEAD - tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company}) - tdr.insert() - tdr.submit() -======= from erpnext.setup.doctype.transaction_deletion_record.transaction_deletion_record import ( is_deletion_doc_running, ) @@ -837,4 +828,3 @@ def create_transaction_deletion_request(company): ), frappe.bold(company), ) ->>>>>>> 5a3afea8c7 (refactor: link running doc validation to company master) diff --git a/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py index 78d6ea67bff5..24a12bac9fef 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/test_transaction_deletion_record.py @@ -27,12 +27,8 @@ def test_doctypes_contain_company_field(self): def test_no_of_docs_is_correct(self): for i in range(5): create_task("Dunder Mifflin Paper Co") -<<<<<<< HEAD tdr = create_transaction_deletion_request("Dunder Mifflin Paper Co") -======= - tdr = create_transaction_deletion_doc("Dunder Mifflin Paper Co") tdr.reload() ->>>>>>> 81309576b0 (refactor(test): test cases modified to handle new approach) for doctype in tdr.doctypes: if doctype.doctype_name == "Task": self.assertEqual(doctype.no_of_docs, 5) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index a144525fe13a..f1eb4e6a9b93 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -12,39 +12,6 @@ class TransactionDeletionRecord(Document): -<<<<<<< HEAD -======= - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - from erpnext.accounts.doctype.transaction_deletion_record_details.transaction_deletion_record_details import ( - TransactionDeletionRecordDetails, - ) - from erpnext.setup.doctype.transaction_deletion_record_item.transaction_deletion_record_item import ( - TransactionDeletionRecordItem, - ) - - amended_from: DF.Link | None - clear_notifications: DF.Check - company: DF.Link - delete_bin_data: DF.Check - delete_leads_and_addresses: DF.Check - delete_transactions: DF.Check - doctypes: DF.Table[TransactionDeletionRecordDetails] - doctypes_to_be_ignored: DF.Table[TransactionDeletionRecordItem] - error_log: DF.LongText | None - initialize_doctypes_table: DF.Check - process_in_single_transaction: DF.Check - reset_company_default_values: DF.Check - status: DF.Literal["Queued", "Running", "Failed", "Completed", "Cancelled"] - # end: auto-generated types - ->>>>>>> 0d65d878de (refactor: more options for 'status' and move it to top) def __init__(self, *args, **kwargs): super(TransactionDeletionRecord, self).__init__(*args, **kwargs) self.batch_size = 5000 diff --git a/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.py b/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.py index ed01afe70f94..92ca8a2ac730 100644 --- a/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.py +++ b/erpnext/setup/doctype/transaction_deletion_record_item/transaction_deletion_record_item.py @@ -7,21 +7,4 @@ class TransactionDeletionRecordItem(Document): -<<<<<<< HEAD -======= - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - doctype_name: DF.Link - parent: DF.Data - parentfield: DF.Data - parenttype: DF.Data - # end: auto-generated types - ->>>>>>> 6a77d86a53 (refactor: use flags to decide on current stage) pass From 5e68ebd030418e589bebc8bdb6f041ef00a20625 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 7 Feb 2024 21:56:21 +0530 Subject: [PATCH 36/94] perf: new column posting datetime in SLE to optimize stock ledger related queries (cherry picked from commit d80ca523a43e9d1073dc008ead7d140fb0811a89) --- .../report/gross_profit/gross_profit.py | 2 +- erpnext/accounts/utils.py | 43 +- erpnext/manufacturing/doctype/bom/bom.py | 3 +- .../work_order_summary/work_order_summary.py | 2 +- erpnext/patches.txt | 3 +- ...te_posting_datetime_and_dropped_indexes.py | 19 + .../purchase_receipt/test_purchase_receipt.py | 91 +- .../test_serial_and_batch_bundle.py | 590 ++++++++ .../stock/doctype/stock_entry/stock_entry.py | 138 ++ .../stock_ledger_entry.json | 10 +- .../stock_ledger_entry/stock_ledger_entry.py | 55 +- .../test_stock_ledger_entry.py | 2 +- .../stock_reservation_entry.py | 1189 +++++++++++++++++ .../incorrect_stock_value_report.py | 4 +- .../product_bundle_balance.py | 4 +- .../report/stock_balance/stock_balance.py | 4 +- .../stock/report/stock_ledger/stock_ledger.py | 2 +- erpnext/stock/stock_ledger.py | 164 ++- erpnext/stock/utils.py | 17 +- 19 files changed, 2218 insertions(+), 124 deletions(-) create mode 100644 erpnext/patches/v14_0/update_posting_datetime_and_dropped_indexes.py create mode 100644 erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py create mode 100644 erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index de3d57d095a8..f5a980019bd8 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -965,7 +965,7 @@ def get_stock_ledger_entries(self, item_code, warehouse): & (sle.is_cancelled == 0) ) .orderby(sle.item_code) - .orderby(sle.warehouse, sle.posting_date, sle.posting_time, sle.creation, order=Order.desc) + .orderby(sle.warehouse, sle.posting_datetime, sle.creation, order=Order.desc) .run(as_dict=True) ) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 4f1f967f202e..b6520d3c82d7 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -931,46 +931,6 @@ def get_currency_precision(): return precision -def get_stock_rbnb_difference(posting_date, company): - stock_items = frappe.db.sql_list( - """select distinct item_code - from `tabStock Ledger Entry` where company=%s""", - company, - ) - - pr_valuation_amount = frappe.db.sql( - """ - select sum(pr_item.valuation_rate * pr_item.qty * pr_item.conversion_factor) - from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr - where pr.name = pr_item.parent and pr.docstatus=1 and pr.company=%s - and pr.posting_date <= %s and pr_item.item_code in (%s)""" - % ("%s", "%s", ", ".join(["%s"] * len(stock_items))), - tuple([company, posting_date] + stock_items), - )[0][0] - - pi_valuation_amount = frappe.db.sql( - """ - select sum(pi_item.valuation_rate * pi_item.qty * pi_item.conversion_factor) - from `tabPurchase Invoice Item` pi_item, `tabPurchase Invoice` pi - where pi.name = pi_item.parent and pi.docstatus=1 and pi.company=%s - and pi.posting_date <= %s and pi_item.item_code in (%s)""" - % ("%s", "%s", ", ".join(["%s"] * len(stock_items))), - tuple([company, posting_date] + stock_items), - )[0][0] - - # Balance should be - stock_rbnb = flt(pr_valuation_amount, 2) - flt(pi_valuation_amount, 2) - - # Balance as per system - stock_rbnb_account = "Stock Received But Not Billed - " + frappe.get_cached_value( - "Company", company, "abbr" - ) - sys_bal = get_balance_on(stock_rbnb_account, posting_date, in_account_currency=False) - - # Amount should be credited - return flt(stock_rbnb) + flt(sys_bal) - - def get_held_invoices(party_type, party): """ Returns a list of names Purchase Invoices for the given party that are on hold @@ -1372,8 +1332,7 @@ def sort_stock_vouchers_by_posting_date( .select(sle.voucher_type, sle.voucher_no, sle.posting_date, sle.posting_time, sle.creation) .where((sle.is_cancelled == 0) & (sle.voucher_no.isin(voucher_nos))) .groupby(sle.voucher_type, sle.voucher_no) - .orderby(sle.posting_date) - .orderby(sle.posting_time) + .orderby(sle.posting_datetime) .orderby(sle.creation) ).run(as_dict=True) sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles] diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index e281fbc1ec99..03190e7b965a 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -978,8 +978,7 @@ def get_valuation_rate(data): frappe.qb.from_(sle) .select(sle.valuation_rate) .where((sle.item_code == item_code) & (sle.valuation_rate > 0) & (sle.is_cancelled == 0)) - .orderby(sle.posting_date, order=frappe.qb.desc) - .orderby(sle.posting_time, order=frappe.qb.desc) + .orderby(sle.posting_datetime, order=frappe.qb.desc) .orderby(sle.creation, order=frappe.qb.desc) .limit(1) ).run(as_dict=True) diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py index 97f30ef62e9d..8d3770805e61 100644 --- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py +++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py @@ -58,7 +58,7 @@ def get_data(filters): query_filters["creation"] = ("between", [filters.get("from_date"), filters.get("to_date")]) data = frappe.get_all( - "Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc", debug=1 + "Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc" ) res = [] diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 9b3cf5d7bc70..e67dad0e8ba3 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -274,6 +274,7 @@ erpnext.patches.v14_0.clear_reconciliation_values_from_singles [post_model_sync] execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings') +erpnext.patches.v14_0.update_posting_datetime_and_dropped_indexes erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents erpnext.patches.v14_0.delete_shopify_doctypes erpnext.patches.v14_0.delete_healthcare_doctypes @@ -361,4 +362,4 @@ erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2 erpnext.patches.v14_0.set_maintain_stock_for_bom_item execute:frappe.db.set_single_value('E Commerce Settings', 'show_actual_qty', 1) erpnext.patches.v14_0.delete_orphaned_asset_movement_item_records -erpnext.patches.v14_0.remove_cancelled_asset_capitalization_from_asset +erpnext.patches.v14_0.remove_cancelled_asset_capitalization_from_asset \ No newline at end of file diff --git a/erpnext/patches/v14_0/update_posting_datetime_and_dropped_indexes.py b/erpnext/patches/v14_0/update_posting_datetime_and_dropped_indexes.py new file mode 100644 index 000000000000..6ec3f842007e --- /dev/null +++ b/erpnext/patches/v14_0/update_posting_datetime_and_dropped_indexes.py @@ -0,0 +1,19 @@ +import frappe + + +def execute(): + frappe.db.sql( + """ + UPDATE `tabStock Ledger Entry` + SET posting_datetime = timestamp(posting_date, posting_time) + """ + ) + + drop_indexes() + + +def drop_indexes(): + if not frappe.db.has_index("tabStock Ledger Entry", "posting_sort_index"): + return + + frappe.db.sql_ddl("ALTER TABLE `tabStock Ledger Entry` DROP INDEX `posting_sort_index`") diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 7defbc5bcdfa..0d244da1fdc2 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -3,7 +3,7 @@ import frappe from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import add_days, cint, cstr, flt, today +from frappe.utils import add_days, cint, cstr, flt, today, nowtime from pypika import functions as fn import erpnext @@ -2224,6 +2224,95 @@ def test_pr_billed_amount_against_return_entry(self): pr.reload() self.assertEqual(pr.per_billed, 100) + def test_sle_qty_after_transaction(self): + item = make_item( + "_Test Item Qty After Transaction", + properties={"is_stock_item": 1, "valuation_method": "FIFO"}, + ).name + + posting_date = today() + posting_time = nowtime() + + # Step 1: Create Purchase Receipt + pr = make_purchase_receipt( + item_code=item, + qty=1, + rate=100, + posting_date=posting_date, + posting_time=posting_time, + do_not_save=1, + ) + + for i in range(9): + pr.append( + "items", + { + "item_code": item, + "qty": 1, + "rate": 100, + "warehouse": pr.items[0].warehouse, + "cost_center": pr.items[0].cost_center, + "expense_account": pr.items[0].expense_account, + "uom": pr.items[0].uom, + "stock_uom": pr.items[0].stock_uom, + "conversion_factor": pr.items[0].conversion_factor, + }, + ) + + self.assertEqual(len(pr.items), 10) + pr.save() + pr.submit() + + data = frappe.get_all( + "Stock Ledger Entry", + fields=["qty_after_transaction", "creation", "posting_datetime"], + filters={"voucher_no": pr.name, "is_cancelled": 0}, + order_by="creation", + ) + + for index, d in enumerate(data): + self.assertEqual(d.qty_after_transaction, 1 + index) + + # Step 2: Create Purchase Receipt + pr = make_purchase_receipt( + item_code=item, + qty=1, + rate=100, + posting_date=posting_date, + posting_time=posting_time, + do_not_save=1, + ) + + for i in range(9): + pr.append( + "items", + { + "item_code": item, + "qty": 1, + "rate": 100, + "warehouse": pr.items[0].warehouse, + "cost_center": pr.items[0].cost_center, + "expense_account": pr.items[0].expense_account, + "uom": pr.items[0].uom, + "stock_uom": pr.items[0].stock_uom, + "conversion_factor": pr.items[0].conversion_factor, + }, + ) + + self.assertEqual(len(pr.items), 10) + pr.save() + pr.submit() + + data = frappe.get_all( + "Stock Ledger Entry", + fields=["qty_after_transaction", "creation", "posting_datetime"], + filters={"voucher_no": pr.name, "is_cancelled": 0}, + order_by="creation", + ) + + for index, d in enumerate(data): + self.assertEqual(d.qty_after_transaction, 11 + index) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py new file mode 100644 index 000000000000..b932c1371d6e --- /dev/null +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -0,0 +1,590 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import json + +import frappe +from frappe.tests.utils import FrappeTestCase, change_settings +from frappe.utils import flt, nowtime, today + +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + add_serial_batch_ledgers, + make_batch_nos, + make_serial_nos, +) +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + +class TestSerialandBatchBundle(FrappeTestCase): + def test_inward_outward_serial_valuation(self): + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + + serial_item_code = "New Serial No Valuation 1" + make_item( + serial_item_code, + { + "has_serial_no": 1, + "serial_no_series": "TEST-SER-VAL-.#####", + "is_stock_item": 1, + }, + ) + + pr = make_purchase_receipt( + item_code=serial_item_code, warehouse="_Test Warehouse - _TC", qty=1, rate=500 + ) + + serial_no1 = get_serial_nos_from_bundle(pr.items[0].serial_and_batch_bundle)[0] + + pr = make_purchase_receipt( + item_code=serial_item_code, warehouse="_Test Warehouse - _TC", qty=1, rate=300 + ) + + serial_no2 = get_serial_nos_from_bundle(pr.items[0].serial_and_batch_bundle)[0] + + dn = create_delivery_note( + item_code=serial_item_code, + warehouse="_Test Warehouse - _TC", + qty=1, + rate=1500, + serial_no=[serial_no2], + ) + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": dn.name, "is_cancelled": 0, "voucher_type": "Delivery Note"}, + "stock_value_difference", + ) + + self.assertEqual(flt(stock_value_difference, 2), -300) + + dn = create_delivery_note( + item_code=serial_item_code, + warehouse="_Test Warehouse - _TC", + qty=1, + rate=1500, + serial_no=[serial_no1], + ) + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": dn.name, "is_cancelled": 0, "voucher_type": "Delivery Note"}, + "stock_value_difference", + ) + + self.assertEqual(flt(stock_value_difference, 2), -500) + + def test_inward_outward_batch_valuation(self): + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + + batch_item_code = "New Batch No Valuation 1" + make_item( + batch_item_code, + { + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TEST-BATTCCH-VAL-.#####", + "is_stock_item": 1, + }, + ) + + pr = make_purchase_receipt( + item_code=batch_item_code, warehouse="_Test Warehouse - _TC", qty=10, rate=500 + ) + + batch_no1 = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle) + + pr = make_purchase_receipt( + item_code=batch_item_code, warehouse="_Test Warehouse - _TC", qty=10, rate=300 + ) + + batch_no2 = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle) + + dn = create_delivery_note( + item_code=batch_item_code, + warehouse="_Test Warehouse - _TC", + qty=10, + rate=1500, + batch_no=batch_no2, + ) + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": dn.name, "is_cancelled": 0, "voucher_type": "Delivery Note"}, + "stock_value_difference", + ) + + self.assertEqual(flt(stock_value_difference, 2), -3000) + + dn = create_delivery_note( + item_code=batch_item_code, + warehouse="_Test Warehouse - _TC", + qty=10, + rate=1500, + batch_no=batch_no1, + ) + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": dn.name, "is_cancelled": 0, "voucher_type": "Delivery Note"}, + "stock_value_difference", + ) + + self.assertEqual(flt(stock_value_difference, 2), -5000) + + def test_old_batch_valuation(self): + frappe.flags.ignore_serial_batch_bundle_validation = True + frappe.flags.use_serial_and_batch_fields = True + batch_item_code = "Old Batch Item Valuation 1" + make_item( + batch_item_code, + { + "has_batch_no": 1, + "is_stock_item": 1, + }, + ) + + batch_id = "Old Batch 1" + if not frappe.db.exists("Batch", batch_id): + batch_doc = frappe.get_doc( + { + "doctype": "Batch", + "batch_id": batch_id, + "item": batch_item_code, + "use_batchwise_valuation": 0, + } + ).insert(ignore_permissions=True) + + self.assertTrue(batch_doc.use_batchwise_valuation) + batch_doc.db_set("use_batchwise_valuation", 0) + + stock_queue = [] + qty_after_transaction = 0 + balance_value = 0 + for qty, valuation in {10: 100, 20: 200}.items(): + stock_queue.append([qty, valuation]) + qty_after_transaction += qty + balance_value += qty_after_transaction * valuation + + doc = frappe.get_doc( + { + "doctype": "Stock Ledger Entry", + "posting_date": today(), + "posting_time": nowtime(), + "batch_no": batch_id, + "incoming_rate": valuation, + "qty_after_transaction": qty_after_transaction, + "stock_value_difference": valuation * qty, + "balance_value": balance_value, + "valuation_rate": balance_value / qty_after_transaction, + "actual_qty": qty, + "item_code": batch_item_code, + "warehouse": "_Test Warehouse - _TC", + "stock_queue": json.dumps(stock_queue), + } + ) + + doc.flags.ignore_permissions = True + doc.flags.ignore_mandatory = True + doc.flags.ignore_links = True + doc.flags.ignore_validate = True + doc.submit() + doc.reload() + + bundle_doc = make_serial_batch_bundle( + { + "item_code": batch_item_code, + "warehouse": "_Test Warehouse - _TC", + "voucher_type": "Stock Entry", + "posting_date": today(), + "posting_time": nowtime(), + "qty": -10, + "batches": frappe._dict({batch_id: 10}), + "type_of_transaction": "Outward", + "do_not_submit": True, + } + ) + + bundle_doc.reload() + for row in bundle_doc.entries: + self.assertEqual(flt(row.stock_value_difference, 2), -1666.67) + + bundle_doc.flags.ignore_permissions = True + bundle_doc.flags.ignore_mandatory = True + bundle_doc.flags.ignore_links = True + bundle_doc.flags.ignore_validate = True + bundle_doc.submit() + + bundle_doc = make_serial_batch_bundle( + { + "item_code": batch_item_code, + "warehouse": "_Test Warehouse - _TC", + "voucher_type": "Stock Entry", + "posting_date": today(), + "posting_time": nowtime(), + "qty": -20, + "batches": frappe._dict({batch_id: 20}), + "type_of_transaction": "Outward", + "do_not_submit": True, + } + ) + + bundle_doc.reload() + for row in bundle_doc.entries: + self.assertEqual(flt(row.stock_value_difference, 2), -3333.33) + + bundle_doc.flags.ignore_permissions = True + bundle_doc.flags.ignore_mandatory = True + bundle_doc.flags.ignore_links = True + bundle_doc.flags.ignore_validate = True + bundle_doc.submit() + + frappe.flags.ignore_serial_batch_bundle_validation = False + frappe.flags.use_serial_and_batch_fields = False + + def test_old_serial_no_valuation(self): + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + + serial_no_item_code = "Old Serial No Item Valuation 1" + make_item( + serial_no_item_code, + { + "has_serial_no": 1, + "serial_no_series": "TEST-SER-VALL-.#####", + "is_stock_item": 1, + }, + ) + + make_purchase_receipt( + item_code=serial_no_item_code, warehouse="_Test Warehouse - _TC", qty=1, rate=500 + ) + + frappe.flags.ignore_serial_batch_bundle_validation = True + frappe.flags.use_serial_and_batch_fields = True + + serial_no_id = "Old Serial No 1" + if not frappe.db.exists("Serial No", serial_no_id): + sn_doc = frappe.get_doc( + { + "doctype": "Serial No", + "serial_no": serial_no_id, + "item_code": serial_no_item_code, + "company": "_Test Company", + } + ).insert(ignore_permissions=True) + + sn_doc.db_set( + { + "warehouse": "_Test Warehouse - _TC", + "purchase_rate": 100, + } + ) + + doc = frappe.get_doc( + { + "doctype": "Stock Ledger Entry", + "posting_date": today(), + "posting_time": nowtime(), + "serial_no": serial_no_id, + "incoming_rate": 100, + "qty_after_transaction": 1, + "stock_value_difference": 100, + "balance_value": 100, + "valuation_rate": 100, + "actual_qty": 1, + "item_code": serial_no_item_code, + "warehouse": "_Test Warehouse - _TC", + "company": "_Test Company", + } + ) + + doc.flags.ignore_permissions = True + doc.flags.ignore_mandatory = True + doc.flags.ignore_links = True + doc.flags.ignore_validate = True + doc.submit() + + bundle_doc = make_serial_batch_bundle( + { + "item_code": serial_no_item_code, + "warehouse": "_Test Warehouse - _TC", + "voucher_type": "Stock Entry", + "posting_date": today(), + "posting_time": nowtime(), + "qty": -1, + "serial_nos": [serial_no_id], + "type_of_transaction": "Outward", + "do_not_submit": True, + } + ) + + bundle_doc.reload() + for row in bundle_doc.entries: + self.assertEqual(flt(row.stock_value_difference, 2), -100.00) + + frappe.flags.ignore_serial_batch_bundle_validation = False + frappe.flags.use_serial_and_batch_fields = False + + def test_batch_not_belong_to_serial_no(self): + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + + serial_and_batch_code = "New Serial No Valuation 1" + make_item( + serial_and_batch_code, + { + "has_serial_no": 1, + "serial_no_series": "TEST-SER-VALL-.#####", + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TEST-SNBAT-VAL-.#####", + }, + ) + + pr = make_purchase_receipt( + item_code=serial_and_batch_code, warehouse="_Test Warehouse - _TC", qty=1, rate=500 + ) + + serial_no = get_serial_nos_from_bundle(pr.items[0].serial_and_batch_bundle)[0] + + pr = make_purchase_receipt( + item_code=serial_and_batch_code, warehouse="_Test Warehouse - _TC", qty=1, rate=300 + ) + + batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle) + + doc = frappe.get_doc( + { + "doctype": "Serial and Batch Bundle", + "item_code": serial_and_batch_code, + "warehouse": "_Test Warehouse - _TC", + "voucher_type": "Stock Entry", + "posting_date": today(), + "posting_time": nowtime(), + "qty": -1, + "type_of_transaction": "Outward", + } + ) + + doc.append( + "entries", + { + "batch_no": batch_no, + "serial_no": serial_no, + "qty": -1, + }, + ) + + # Batch does not belong to serial no + self.assertRaises(frappe.exceptions.ValidationError, doc.save) + + def test_auto_delete_draft_serial_and_batch_bundle(self): + serial_and_batch_code = "New Serial No Auto Delete 1" + make_item( + serial_and_batch_code, + { + "has_serial_no": 1, + "serial_no_series": "TEST-SER-VALL-.#####", + "is_stock_item": 1, + }, + ) + + ste = make_stock_entry( + item_code=serial_and_batch_code, + target="_Test Warehouse - _TC", + qty=1, + rate=500, + do_not_submit=True, + ) + + serial_no = "SN-TEST-AUTO-DEL" + if not frappe.db.exists("Serial No", serial_no): + frappe.get_doc( + { + "doctype": "Serial No", + "serial_no": serial_no, + "item_code": serial_and_batch_code, + "company": "_Test Company", + } + ).insert(ignore_permissions=True) + + bundle_doc = make_serial_batch_bundle( + { + "item_code": serial_and_batch_code, + "warehouse": "_Test Warehouse - _TC", + "voucher_type": "Stock Entry", + "posting_date": ste.posting_date, + "posting_time": ste.posting_time, + "qty": 1, + "serial_nos": [serial_no], + "type_of_transaction": "Inward", + "do_not_submit": True, + } + ) + + bundle_doc.reload() + ste.items[0].serial_and_batch_bundle = bundle_doc.name + ste.save() + ste.reload() + + ste.delete() + self.assertFalse(frappe.db.exists("Serial and Batch Bundle", bundle_doc.name)) + + def test_serial_and_batch_bundle_company(self): + 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, + warehouse="_Test Warehouse - _TC", + qty=3, + rate=500, + do_not_submit=True, + ) + + entries = [] + for serial_no in ["TT-SER-VAL-00001", "TT-SER-VAL-00002", "TT-SER-VAL-00003"]: + entries.append(frappe._dict({"serial_no": serial_no, "qty": 1})) + + if not frappe.db.exists("Serial No", serial_no): + frappe.get_doc( + { + "doctype": "Serial No", + "serial_no": serial_no, + "item_code": item, + } + ).insert(ignore_permissions=True) + + item_row = pr.items[0] + item_row.type_of_transaction = "Inward" + item_row.is_rejected = 0 + 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 test_batch_duplicate_entry(self): + item_code = make_item(properties={"has_batch_no": 1}).name + + batch_id = "TEST-BATTCCH-VAL-00001" + batch_nos = [{"batch_no": batch_id, "qty": 1}] + + make_batch_nos(item_code, batch_nos) + self.assertTrue(frappe.db.exists("Batch", batch_id)) + + batch_id = "TEST-BATTCCH-VAL-00001" + batch_nos = [{"batch_no": batch_id, "qty": 1}] + + # Shouldn't throw duplicate entry error + make_batch_nos(item_code, batch_nos) + self.assertTrue(frappe.db.exists("Batch", batch_id)) + + def test_serial_no_duplicate_entry(self): + item_code = make_item(properties={"has_serial_no": 1}).name + + serial_no_id = "TEST-SNID-VAL-00001" + serial_nos = [{"serial_no": serial_no_id, "qty": 1}] + + make_serial_nos(item_code, serial_nos) + self.assertTrue(frappe.db.exists("Serial No", serial_no_id)) + + serial_no_id = "TEST-SNID-VAL-00001" + serial_nos = [{"batch_no": serial_no_id, "qty": 1}] + + # Shouldn't throw duplicate entry error + make_serial_nos(item_code, serial_nos) + self.assertTrue(frappe.db.exists("Serial No", serial_no_id)) + + @change_settings("Stock Settings", {"auto_create_serial_and_batch_bundle_for_outward": 1}) + def test_duplicate_serial_and_batch_bundle(self): + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + + item_code = make_item(properties={"is_stock_item": 1, "has_serial_no": 1}).name + + serial_no = f"{item_code}-001" + serial_nos = [{"serial_no": serial_no, "qty": 1}] + make_serial_nos(item_code, serial_nos) + + pr1 = make_purchase_receipt(item=item_code, qty=1, rate=500, serial_no=[serial_no]) + pr2 = make_purchase_receipt(item=item_code, qty=1, rate=500, do_not_save=True) + + pr1.reload() + pr2.items[0].serial_and_batch_bundle = pr1.items[0].serial_and_batch_bundle + + self.assertRaises(frappe.exceptions.ValidationError, pr2.save) + + +def get_batch_from_bundle(bundle): + from erpnext.stock.serial_batch_bundle import get_batch_nos + + batches = get_batch_nos(bundle) + + return list(batches.keys())[0] + + +def get_serial_nos_from_bundle(bundle): + from erpnext.stock.serial_batch_bundle import get_serial_nos + + serial_nos = get_serial_nos(bundle) + return sorted(serial_nos) if serial_nos else [] + + +def make_serial_batch_bundle(kwargs): + from erpnext.stock.serial_batch_bundle import SerialBatchCreation + + if isinstance(kwargs, dict): + kwargs = frappe._dict(kwargs) + + type_of_transaction = "Inward" if kwargs.qty > 0 else "Outward" + if kwargs.get("type_of_transaction"): + type_of_transaction = kwargs.get("type_of_transaction") + + sb = SerialBatchCreation( + { + "item_code": kwargs.item_code, + "warehouse": kwargs.warehouse, + "voucher_type": kwargs.voucher_type, + "voucher_no": kwargs.voucher_no, + "posting_date": kwargs.posting_date, + "posting_time": kwargs.posting_time, + "qty": kwargs.qty, + "avg_rate": kwargs.rate, + "batches": kwargs.batches, + "serial_nos": kwargs.serial_nos, + "type_of_transaction": type_of_transaction, + "company": kwargs.company or "_Test Company", + "do_not_submit": kwargs.do_not_submit, + } + ) + + if not kwargs.get("do_not_save"): + return sb.make_serial_and_batch_bundle() + + return sb diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index ca31a9a3d31c..408aeb12be17 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1721,9 +1721,22 @@ def add_batchwise_finished_good(self, data, args, item): if qty <= 0: break +<<<<<<< HEAD fg_qty = batch_qty if batch_qty >= qty: fg_qty = qty +======= + id = create_serial_and_batch_bundle( + self, + row, + frappe._dict( + { + "item_code": self.pro_doc.production_item, + "warehouse": args.get("to_warehouse"), + } + ), + ) +>>>>>>> d80ca523a4 (perf: new column posting datetime in SLE to optimize stock ledger related queries) qty -= batch_qty args["qty"] = fg_qty @@ -1969,7 +1982,11 @@ def update_item_in_stock_entry_detail(self, row, item, qty) -> None: "to_warehouse": "", "qty": qty, "item_name": item.item_name, +<<<<<<< HEAD "batch_no": item.batch_no, +======= + "serial_and_batch_bundle": create_serial_and_batch_bundle(self, row, item, "Outward"), +>>>>>>> d80ca523a4 (perf: new column posting datetime in SLE to optimize stock ledger related queries) "description": item.description, "stock_uom": item.stock_uom, "expense_account": item.expense_account, @@ -2377,6 +2394,39 @@ def set_material_request_transfer_status(self, status): frappe.db.set_value("Material Request", material_request, "transfer_status", status) def set_serial_no_batch_for_finished_good(self): +<<<<<<< HEAD +======= + if not ( + (self.pro_doc.has_serial_no or self.pro_doc.has_batch_no) + and frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order") + ): + return + + for d in self.items: + if ( + d.is_finished_item + and d.item_code == self.pro_doc.production_item + and not d.serial_and_batch_bundle + ): + serial_nos = self.get_available_serial_nos() + if serial_nos: + row = frappe._dict({"serial_nos": serial_nos[0 : cint(d.qty)]}) + + id = create_serial_and_batch_bundle( + self, + row, + frappe._dict( + { + "item_code": d.item_code, + "warehouse": d.t_warehouse, + } + ), + ) + + d.serial_and_batch_bundle = id + + def get_available_serial_nos(self) -> List[str]: +>>>>>>> d80ca523a4 (perf: new column posting datetime in SLE to optimize stock ledger related queries) serial_nos = [] if self.pro_doc.serial_no: serial_nos = self.get_serial_nos_for_fg() or [] @@ -2855,3 +2905,91 @@ def get_stock_entry_data(work_order): ) .orderby(stock_entry.creation, stock_entry_detail.item_code, stock_entry_detail.idx) ).run(as_dict=1) +<<<<<<< HEAD +======= + + if not data: + return [] + + voucher_nos = [row.get("name") for row in data if row.get("name")] + if voucher_nos: + bundle_data = get_voucher_wise_serial_batch_from_bundle(voucher_no=voucher_nos) + for row in data: + key = (row.item_code, row.warehouse, row.name) + if row.purpose != "Material Transfer for Manufacture": + key = (row.item_code, row.s_warehouse, row.name) + + if bundle_data.get(key): + row.update(bundle_data.get(key)) + + return data + + +def create_serial_and_batch_bundle(parent_doc, row, child, type_of_transaction=None): + item_details = frappe.get_cached_value( + "Item", child.item_code, ["has_serial_no", "has_batch_no"], as_dict=1 + ) + + if not (item_details.has_serial_no or item_details.has_batch_no): + return + + if not type_of_transaction: + type_of_transaction = "Inward" + + doc = frappe.get_doc( + { + "doctype": "Serial and Batch Bundle", + "voucher_type": "Stock Entry", + "item_code": child.item_code, + "warehouse": child.warehouse, + "type_of_transaction": type_of_transaction, + "posting_date": parent_doc.posting_date, + "posting_time": parent_doc.posting_time, + } + ) + + if row.serial_nos and row.batches_to_be_consume: + doc.has_serial_no = 1 + doc.has_batch_no = 1 + batchwise_serial_nos = get_batchwise_serial_nos(child.item_code, row) + for batch_no, qty in row.batches_to_be_consume.items(): + + while qty > 0: + qty -= 1 + doc.append( + "entries", + { + "batch_no": batch_no, + "serial_no": batchwise_serial_nos.get(batch_no).pop(0), + "warehouse": row.warehouse, + "qty": -1, + }, + ) + + elif row.serial_nos: + doc.has_serial_no = 1 + for serial_no in row.serial_nos: + doc.append("entries", {"serial_no": serial_no, "warehouse": row.warehouse, "qty": -1}) + + elif row.batches_to_be_consume: + doc.has_batch_no = 1 + for batch_no, qty in row.batches_to_be_consume.items(): + doc.append("entries", {"batch_no": batch_no, "warehouse": row.warehouse, "qty": qty * -1}) + + return doc.insert(ignore_permissions=True).name + + +def get_batchwise_serial_nos(item_code, row): + batchwise_serial_nos = {} + + for batch_no in row.batches_to_be_consume: + serial_nos = frappe.get_all( + "Serial No", + filters={"item_code": item_code, "batch_no": batch_no, "name": ("in", row.serial_nos)}, + ) + + if serial_nos: + batchwise_serial_nos[batch_no] = sorted([serial_no.name for serial_no in serial_nos]) + + return batchwise_serial_nos +>>>>>>> d80ca523a4 (perf: new column posting datetime in SLE to optimize stock ledger related queries) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json index 0a666b44fbd2..835002f0e16a 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json @@ -11,6 +11,7 @@ "warehouse", "posting_date", "posting_time", + "posting_datetime", "is_adjustment_entry", "column_break_6", "voucher_type", @@ -96,7 +97,6 @@ "oldfieldtype": "Date", "print_width": "100px", "read_only": 1, - "search_index": 1, "width": "100px" }, { @@ -249,7 +249,6 @@ "options": "Company", "print_width": "150px", "read_only": 1, - "search_index": 1, "width": "150px" }, { @@ -316,6 +315,11 @@ "fieldname": "is_adjustment_entry", "fieldtype": "Check", "label": "Is Adjustment Entry" + }, + { + "fieldname": "posting_datetime", + "fieldtype": "Datetime", + "label": "Posting Datetime" } ], "hide_toolbar": 1, @@ -324,7 +328,7 @@ "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2024-03-13 09:56:13.021696", + "modified": "2024-02-07 09:18:13.999231", "modified_by": "Administrator", "module": "Stock", "name": "Stock Ledger Entry", diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 9580e83ed956..8864ef4b2f29 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -28,6 +28,50 @@ class BackDatedStockTransaction(frappe.ValidationError): class StockLedgerEntry(Document): +<<<<<<< HEAD +======= + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + actual_qty: DF.Float + auto_created_serial_and_batch_bundle: DF.Check + batch_no: DF.Data | None + company: DF.Link | None + dependant_sle_voucher_detail_no: DF.Data | None + fiscal_year: DF.Data | None + has_batch_no: DF.Check + has_serial_no: DF.Check + incoming_rate: DF.Currency + is_adjustment_entry: DF.Check + is_cancelled: DF.Check + item_code: DF.Link | None + outgoing_rate: DF.Currency + posting_date: DF.Date | None + posting_datetime: DF.Datetime | None + posting_time: DF.Time | None + project: DF.Link | None + qty_after_transaction: DF.Float + recalculate_rate: DF.Check + serial_and_batch_bundle: DF.Link | None + serial_no: DF.LongText | None + stock_queue: DF.Text | None + stock_uom: DF.Link | None + stock_value: DF.Currency + stock_value_difference: DF.Currency + to_rename: DF.Check + valuation_rate: DF.Currency + voucher_detail_no: DF.Data | None + voucher_no: DF.DynamicLink | None + voucher_type: DF.Link | None + warehouse: DF.Link | None + # end: auto-generated types + +>>>>>>> d80ca523a4 (perf: new column posting datetime in SLE to optimize stock ledger related queries) def autoname(self): """ Temporarily name doc for fast insertion @@ -52,6 +96,12 @@ def validate(self): self.validate_with_last_transaction_posting_time() self.validate_inventory_dimension_negative_stock() + def set_posting_datetime(self): + from erpnext.stock.utils import get_combine_datetime + + self.posting_datetime = get_combine_datetime(self.posting_date, self.posting_time) + self.db_set("posting_datetime", self.posting_datetime) + def validate_inventory_dimension_negative_stock(self): if self.is_cancelled: return @@ -122,6 +172,7 @@ def _get_inventory_dimensions(self): return inv_dimension_dict def on_submit(self): + self.set_posting_datetime() self.check_stock_frozen_date() self.calculate_batch_qty() @@ -293,9 +344,7 @@ def on_cancel(self): def on_doctype_update(): - frappe.db.add_index( - "Stock Ledger Entry", fields=["posting_date", "posting_time"], index_name="posting_sort_index" - ) frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"]) frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"]) frappe.db.add_index("Stock Ledger Entry", ["warehouse", "item_code"], "item_warehouse") + frappe.db.add_index("Stock Ledger Entry", ["posting_datetime", "creation"]) diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index 6c341d9e9ec2..44ebcda1ed20 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -1066,7 +1066,7 @@ def ordered_qty_after_transaction(): frappe.qb.from_(sle) .select("qty_after_transaction") .where((sle.item_code == item) & (sle.warehouse == warehouse) & (sle.is_cancelled == 0)) - .orderby(CombineDatetime(sle.posting_date, sle.posting_time)) + .orderby(sle.posting_datetime) .orderby(sle.creation) ).run(pluck=True) diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py new file mode 100644 index 000000000000..26fe8e1787c2 --- /dev/null +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -0,0 +1,1189 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from typing import Literal + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.query_builder.functions import Sum +from frappe.utils import cint, flt, nowdate, nowtime + +from erpnext.stock.utils import get_or_make_bin, get_stock_balance + + +class StockReservationEntry(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from erpnext.stock.doctype.serial_and_batch_entry.serial_and_batch_entry import ( + SerialandBatchEntry, + ) + + amended_from: DF.Link | None + available_qty: DF.Float + company: DF.Link | None + delivered_qty: DF.Float + from_voucher_detail_no: DF.Data | None + from_voucher_no: DF.DynamicLink | None + from_voucher_type: DF.Literal["", "Pick List", "Purchase Receipt"] + has_batch_no: DF.Check + has_serial_no: DF.Check + item_code: DF.Link | None + project: DF.Link | None + reservation_based_on: DF.Literal["Qty", "Serial and Batch"] + reserved_qty: DF.Float + sb_entries: DF.Table[SerialandBatchEntry] + status: DF.Literal[ + "Draft", "Partially Reserved", "Reserved", "Partially Delivered", "Delivered", "Cancelled" + ] + stock_uom: DF.Link | None + voucher_detail_no: DF.Data | None + voucher_no: DF.DynamicLink | None + voucher_qty: DF.Float + voucher_type: DF.Literal["", "Sales Order"] + warehouse: DF.Link | None + # end: auto-generated types + + def validate(self) -> None: + from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company + + self.validate_amended_doc() + self.validate_mandatory() + self.validate_group_warehouse() + validate_disabled_warehouse(self.warehouse) + validate_warehouse_company(self.warehouse, self.company) + self.validate_uom_is_integer() + + def before_submit(self) -> None: + self.set_reservation_based_on() + self.validate_reservation_based_on_qty() + self.auto_reserve_serial_and_batch() + self.validate_reservation_based_on_serial_and_batch() + + def on_submit(self) -> None: + self.update_reserved_qty_in_voucher() + self.update_reserved_qty_in_pick_list() + self.update_status() + self.update_reserved_stock_in_bin() + + def on_update_after_submit(self) -> None: + self.can_be_updated() + self.validate_uom_is_integer() + self.set_reservation_based_on() + self.validate_reservation_based_on_qty() + self.validate_reservation_based_on_serial_and_batch() + self.update_reserved_qty_in_voucher() + self.update_status() + self.update_reserved_stock_in_bin() + self.reload() + + def on_cancel(self) -> None: + self.update_reserved_qty_in_voucher() + self.update_reserved_qty_in_pick_list() + self.update_status() + self.update_reserved_stock_in_bin() + + def validate_amended_doc(self) -> None: + """Raises an exception if document is amended.""" + + if self.amended_from: + msg = _("Cannot amend {0} {1}, please create a new one instead.").format( + self.doctype, frappe.bold(self.amended_from) + ) + frappe.throw(msg) + + def validate_mandatory(self) -> None: + """Raises an exception if mandatory fields are not set.""" + + mandatory = [ + "item_code", + "warehouse", + "voucher_type", + "voucher_no", + "voucher_detail_no", + "available_qty", + "voucher_qty", + "stock_uom", + "reserved_qty", + "company", + ] + for d in mandatory: + if not self.get(d): + msg = _("{0} is required").format(self.meta.get_label(d)) + frappe.throw(msg) + + def validate_group_warehouse(self) -> None: + """Raises an exception if `Warehouse` is a Group Warehouse.""" + + if frappe.get_cached_value("Warehouse", self.warehouse, "is_group"): + msg = _("Stock cannot be reserved in group warehouse {0}.").format(frappe.bold(self.warehouse)) + frappe.throw(msg, title=_("Invalid Warehouse")) + + def validate_uom_is_integer(self) -> None: + """Validates `Reserved Qty` with Stock UOM.""" + + if cint(frappe.db.get_value("UOM", self.stock_uom, "must_be_whole_number", cache=True)): + if cint(self.reserved_qty) != flt(self.reserved_qty, self.precision("reserved_qty")): + msg = _( + "Reserved Qty ({0}) cannot be a fraction. To allow this, disable '{1}' in UOM {3}." + ).format( + flt(self.reserved_qty, self.precision("reserved_qty")), + frappe.bold(_("Must be Whole Number")), + frappe.bold(self.stock_uom), + ) + frappe.throw(msg) + + def set_reservation_based_on(self) -> None: + """Sets `Reservation Based On` based on `Has Serial No` and `Has Batch No`.""" + + if (self.reservation_based_on == "Serial and Batch") and ( + not self.has_serial_no and not self.has_batch_no + ): + self.db_set("reservation_based_on", "Qty") + + def validate_reservation_based_on_qty(self) -> None: + """Validates `Reserved Qty` when `Reservation Based On` is `Qty`.""" + + if self.reservation_based_on == "Qty": + self.validate_with_allowed_qty(self.reserved_qty) + + def auto_reserve_serial_and_batch(self, based_on: str = None) -> None: + """Auto pick Serial and Batch Nos to reserve when `Reservation Based On` is `Serial and Batch`.""" + + if ( + not self.from_voucher_type + and (self.get("_action") == "submit") + and (self.has_serial_no or self.has_batch_no) + and cint(frappe.db.get_single_value("Stock Settings", "auto_reserve_serial_and_batch")) + ): + from erpnext.stock.doctype.batch.batch import get_available_batches + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos_for_outward + from erpnext.stock.serial_batch_bundle import get_serial_nos_batch + + self.reservation_based_on = "Serial and Batch" + self.sb_entries.clear() + kwargs = frappe._dict( + { + "item_code": self.item_code, + "warehouse": self.warehouse, + "qty": abs(self.reserved_qty) or 0, + "based_on": based_on + or frappe.db.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"), + } + ) + + serial_nos, batch_nos = [], [] + if self.has_serial_no: + serial_nos = get_serial_nos_for_outward(kwargs) + if self.has_batch_no: + batch_nos = get_available_batches(kwargs) + + if serial_nos: + serial_no_wise_batch = frappe._dict({}) + + if self.has_batch_no: + serial_no_wise_batch = get_serial_nos_batch(serial_nos) + + for serial_no in serial_nos: + self.append( + "sb_entries", + { + "serial_no": serial_no, + "qty": 1, + "batch_no": serial_no_wise_batch.get(serial_no), + "warehouse": self.warehouse, + }, + ) + elif batch_nos: + for batch_no, batch_qty in batch_nos.items(): + self.append( + "sb_entries", + { + "batch_no": batch_no, + "qty": batch_qty, + "warehouse": self.warehouse, + }, + ) + + def validate_reservation_based_on_serial_and_batch(self) -> None: + """Validates `Reserved Qty`, `Serial and Batch Nos` when `Reservation Based On` is `Serial and Batch`.""" + + if self.reservation_based_on == "Serial and Batch": + allow_partial_reservation = frappe.db.get_single_value( + "Stock Settings", "allow_partial_reservation" + ) + + available_serial_nos = [] + if self.has_serial_no: + available_serial_nos = get_available_serial_nos_to_reserve( + self.item_code, self.warehouse, self.has_batch_no, ignore_sre=self.name + ) + + if not available_serial_nos: + msg = _("Stock not available for Item {0} in Warehouse {1}.").format( + frappe.bold(self.item_code), frappe.bold(self.warehouse) + ) + frappe.throw(msg) + + qty_to_be_reserved = 0 + selected_batch_nos, selected_serial_nos = [], [] + for entry in self.sb_entries: + entry.warehouse = self.warehouse + + if self.has_serial_no: + entry.qty = 1 + + key = ( + (entry.serial_no, self.warehouse, entry.batch_no) + if self.has_batch_no + else (entry.serial_no, self.warehouse) + ) + if key not in available_serial_nos: + msg = _( + "Row #{0}: Serial No {1} for Item {2} is not available in {3} {4} or might be reserved in another {5}." + ).format( + entry.idx, + frappe.bold(entry.serial_no), + frappe.bold(self.item_code), + _("Batch {0} and Warehouse").format(frappe.bold(entry.batch_no)) + if self.has_batch_no + else _("Warehouse"), + frappe.bold(self.warehouse), + frappe.bold("Stock Reservation Entry"), + ) + + frappe.throw(msg) + + if entry.serial_no in selected_serial_nos: + msg = _("Row #{0}: Serial No {1} is already selected.").format( + entry.idx, frappe.bold(entry.serial_no) + ) + frappe.throw(msg) + else: + selected_serial_nos.append(entry.serial_no) + + elif self.has_batch_no: + if cint(frappe.db.get_value("Batch", entry.batch_no, "disabled")): + msg = _( + "Row #{0}: Stock cannot be reserved for Item {1} against a disabled Batch {2}." + ).format( + entry.idx, frappe.bold(self.item_code), frappe.bold(entry.batch_no) + ) + frappe.throw(msg) + + available_qty_to_reserve = get_available_qty_to_reserve( + self.item_code, self.warehouse, entry.batch_no, ignore_sre=self.name + ) + + if available_qty_to_reserve <= 0: + msg = _( + "Row #{0}: Stock not available to reserve for Item {1} against Batch {2} in Warehouse {3}." + ).format( + entry.idx, + frappe.bold(self.item_code), + frappe.bold(entry.batch_no), + frappe.bold(self.warehouse), + ) + frappe.throw(msg) + + if entry.qty > available_qty_to_reserve: + if allow_partial_reservation: + entry.qty = available_qty_to_reserve + if self.get("_action") == "update_after_submit": + entry.db_update() + else: + msg = _( + "Row #{0}: Qty should be less than or equal to Available Qty to Reserve (Actual Qty - Reserved Qty) {1} for Iem {2} against Batch {3} in Warehouse {4}." + ).format( + entry.idx, + frappe.bold(available_qty_to_reserve), + frappe.bold(self.item_code), + frappe.bold(entry.batch_no), + frappe.bold(self.warehouse), + ) + frappe.throw(msg) + + if entry.batch_no in selected_batch_nos: + msg = _("Row #{0}: Batch No {1} is already selected.").format( + entry.idx, frappe.bold(entry.batch_no) + ) + frappe.throw(msg) + else: + selected_batch_nos.append(entry.batch_no) + + qty_to_be_reserved += entry.qty + + if not qty_to_be_reserved: + msg = _("Please select Serial/Batch Nos to reserve or change Reservation Based On to Qty.") + frappe.throw(msg) + + # Should be called after validating Serial and Batch Nos. + self.validate_with_allowed_qty(qty_to_be_reserved) + self.db_set("reserved_qty", qty_to_be_reserved) + + def update_reserved_qty_in_voucher( + self, reserved_qty_field: str = "stock_reserved_qty", update_modified: bool = True + ) -> None: + """Updates total reserved qty in the voucher.""" + + item_doctype = "Sales Order Item" if self.voucher_type == "Sales Order" else None + + if item_doctype: + sre = frappe.qb.DocType("Stock Reservation Entry") + reserved_qty = ( + frappe.qb.from_(sre) + .select(Sum(sre.reserved_qty)) + .where( + (sre.docstatus == 1) + & (sre.voucher_type == self.voucher_type) + & (sre.voucher_no == self.voucher_no) + & (sre.voucher_detail_no == self.voucher_detail_no) + ) + ).run(as_list=True)[0][0] or 0 + + frappe.db.set_value( + item_doctype, + self.voucher_detail_no, + reserved_qty_field, + reserved_qty, + update_modified=update_modified, + ) + + def update_reserved_qty_in_pick_list( + self, reserved_qty_field: str = "stock_reserved_qty", update_modified: bool = True + ) -> None: + """Updates total reserved qty in the Pick List.""" + + if ( + self.from_voucher_type == "Pick List" and self.from_voucher_no and self.from_voucher_detail_no + ): + sre = frappe.qb.DocType("Stock Reservation Entry") + reserved_qty = ( + frappe.qb.from_(sre) + .select(Sum(sre.reserved_qty)) + .where( + (sre.docstatus == 1) + & (sre.from_voucher_type == "Pick List") + & (sre.from_voucher_no == self.from_voucher_no) + & (sre.from_voucher_detail_no == self.from_voucher_detail_no) + ) + ).run(as_list=True)[0][0] or 0 + + frappe.db.set_value( + "Pick List Item", + self.from_voucher_detail_no, + reserved_qty_field, + reserved_qty, + update_modified=update_modified, + ) + + def update_reserved_stock_in_bin(self) -> None: + """Updates `Reserved Stock` in Bin.""" + + bin_name = get_or_make_bin(self.item_code, self.warehouse) + bin_doc = frappe.get_cached_doc("Bin", bin_name) + bin_doc.update_reserved_stock() + + def update_status(self, status: str = None, update_modified: bool = True) -> None: + """Updates status based on Voucher Qty, Reserved Qty and Delivered Qty.""" + + if not status: + if self.docstatus == 2: + status = "Cancelled" + elif self.docstatus == 1: + if self.reserved_qty == self.delivered_qty: + status = "Delivered" + elif self.delivered_qty and self.delivered_qty < self.reserved_qty: + status = "Partially Delivered" + elif self.reserved_qty == self.voucher_qty: + status = "Reserved" + else: + status = "Partially Reserved" + else: + status = "Draft" + + frappe.db.set_value(self.doctype, self.name, "status", status, update_modified=update_modified) + + def can_be_updated(self) -> None: + """Raises an exception if `Stock Reservation Entry` is not allowed to be updated.""" + + if self.status in ("Partially Delivered", "Delivered"): + msg = _( + "{0} {1} cannot be updated. If you need to make changes, we recommend canceling the existing entry and creating a new one." + ).format(self.status, self.doctype) + frappe.throw(msg) + + if self.from_voucher_type == "Pick List": + msg = _( + "Stock Reservation Entry created against a Pick List cannot be updated. If you need to make changes, we recommend canceling the existing entry and creating a new one." + ) + frappe.throw(msg) + + if self.delivered_qty > 0: + msg = _("Stock Reservation Entry cannot be updated as it has been delivered.") + frappe.throw(msg) + + def validate_with_allowed_qty(self, qty_to_be_reserved: float) -> None: + """Validates `Reserved Qty` with `Max Reserved Qty`.""" + + self.db_set( + "available_qty", + get_available_qty_to_reserve(self.item_code, self.warehouse, ignore_sre=self.name), + ) + + total_reserved_qty = get_sre_reserved_qty_for_voucher_detail_no( + self.voucher_type, self.voucher_no, self.voucher_detail_no, ignore_sre=self.name + ) + + voucher_delivered_qty = 0 + if self.voucher_type == "Sales Order": + delivered_qty, conversion_factor = frappe.db.get_value( + "Sales Order Item", self.voucher_detail_no, ["delivered_qty", "conversion_factor"] + ) + voucher_delivered_qty = flt(delivered_qty) * flt(conversion_factor) + + allowed_qty = min( + self.available_qty, (self.voucher_qty - voucher_delivered_qty - total_reserved_qty) + ) + + if self.get("_action") != "submit" and self.voucher_type == "Sales Order" and allowed_qty <= 0: + msg = _("Item {0} is already reserved/delivered against Sales Order {1}.").format( + frappe.bold(self.item_code), frappe.bold(self.voucher_no) + ) + + if self.docstatus == 1: + self.cancel() + return frappe.msgprint(msg) + else: + frappe.throw(msg) + + if qty_to_be_reserved > allowed_qty: + actual_qty = get_stock_balance(self.item_code, self.warehouse) + msg = """ + Cannot reserve more than Allowed Qty {0} {1} for Item {2} against {3} {4}.

+ The Allowed Qty is calculated as follows:
+
    +
  • Actual Qty [Available Qty at Warehouse] = {5}
  • +
  • Reserved Stock [Ignore current SRE] = {6}
  • +
  • Available Qty To Reserve [Actual Qty - Reserved Stock] = {7}
  • +
  • Voucher Qty [Voucher Item Qty] = {8}
  • +
  • Delivered Qty [Qty delivered against the Voucher Item] = {9}
  • +
  • Total Reserved Qty [Qty reserved against the Voucher Item] = {10}
  • +
  • Allowed Qty [Minimum of (Available Qty To Reserve, (Voucher Qty - Delivered Qty - Total Reserved Qty))] = {11}
  • +
+ """.format( + frappe.bold(allowed_qty), + self.stock_uom, + frappe.bold(self.item_code), + self.voucher_type, + frappe.bold(self.voucher_no), + actual_qty, + actual_qty - self.available_qty, + self.available_qty, + self.voucher_qty, + voucher_delivered_qty, + total_reserved_qty, + allowed_qty, + ) + frappe.throw(msg) + + if qty_to_be_reserved <= self.delivered_qty: + msg = _("Reserved Qty should be greater than Delivered Qty.") + frappe.throw(msg) + + +def validate_stock_reservation_settings(voucher: object) -> None: + """Raises an exception if `Stock Reservation` is not enabled or `Voucher Type` is not allowed.""" + + if not frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"): + msg = _("Please enable {0} in the {1}.").format( + frappe.bold("Stock Reservation"), frappe.bold("Stock Settings") + ) + frappe.throw(msg) + + # Voucher types allowed for stock reservation + allowed_voucher_types = ["Sales Order"] + + if voucher.doctype not in allowed_voucher_types: + msg = _("Stock Reservation can only be created against {0}.").format( + ", ".join(allowed_voucher_types) + ) + frappe.throw(msg) + + +def get_available_qty_to_reserve( + item_code: str, warehouse: str, batch_no: str = None, ignore_sre=None +) -> float: + """Returns `Available Qty to Reserve (Actual Qty - Reserved Qty)` for Item, Warehouse and Batch combination.""" + + from erpnext.stock.doctype.batch.batch import get_batch_qty + + if batch_no: + return get_batch_qty( + item_code=item_code, warehouse=warehouse, batch_no=batch_no, ignore_voucher_nos=[ignore_sre] + ) + + available_qty = get_stock_balance(item_code, warehouse) + + if available_qty: + sre = frappe.qb.DocType("Stock Reservation Entry") + query = ( + frappe.qb.from_(sre) + .select(Sum(sre.reserved_qty - sre.delivered_qty)) + .where( + (sre.docstatus == 1) + & (sre.item_code == item_code) + & (sre.warehouse == warehouse) + & (sre.reserved_qty >= sre.delivered_qty) + & (sre.status.notin(["Delivered", "Cancelled"])) + ) + ) + + if ignore_sre: + query = query.where(sre.name != ignore_sre) + + reserved_qty = query.run()[0][0] or 0.0 + + if reserved_qty: + return available_qty - reserved_qty + + return available_qty + + +def get_available_serial_nos_to_reserve( + item_code: str, warehouse: str, has_batch_no: bool = False, ignore_sre=None +) -> list[tuple]: + """Returns Available Serial Nos to Reserve (Available Serial Nos - Reserved Serial Nos)` for Item, Warehouse and Batch combination.""" + + from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + get_available_serial_nos, + ) + + available_serial_nos = get_available_serial_nos( + frappe._dict( + { + "item_code": item_code, + "warehouse": warehouse, + "has_batch_no": has_batch_no, + "ignore_voucher_nos": [ignore_sre], + } + ) + ) + + available_serial_nos_list = [] + if available_serial_nos: + available_serial_nos_list = [tuple(d.values()) for d in available_serial_nos] + + sre = frappe.qb.DocType("Stock Reservation Entry") + sb_entry = frappe.qb.DocType("Serial and Batch Entry") + query = ( + frappe.qb.from_(sre) + .left_join(sb_entry) + .on(sre.name == sb_entry.parent) + .select(sb_entry.serial_no, sre.warehouse) + .where( + (sre.docstatus == 1) + & (sre.item_code == item_code) + & (sre.warehouse == warehouse) + & (sre.reserved_qty >= sre.delivered_qty) + & (sre.status.notin(["Delivered", "Cancelled"])) + & (sre.reservation_based_on == "Serial and Batch") + ) + ) + + if has_batch_no: + query = query.select(sb_entry.batch_no) + + if ignore_sre: + query = query.where(sre.name != ignore_sre) + + reserved_serial_nos = query.run() + + if reserved_serial_nos: + return list(set(available_serial_nos_list) - set(reserved_serial_nos)) + + return available_serial_nos_list + + +def get_sre_reserved_qty_for_item_and_warehouse(item_code: str, warehouse: str = None) -> float: + """Returns current `Reserved Qty` for Item and Warehouse combination.""" + + sre = frappe.qb.DocType("Stock Reservation Entry") + query = ( + frappe.qb.from_(sre) + .select(Sum(sre.reserved_qty - sre.delivered_qty).as_("reserved_qty")) + .where( + (sre.docstatus == 1) + & (sre.item_code == item_code) + & (sre.status.notin(["Delivered", "Cancelled"])) + ) + .groupby(sre.item_code, sre.warehouse) + ) + + if warehouse: + query = query.where(sre.warehouse == warehouse) + + reserved_qty = query.run(as_list=True) + + return flt(reserved_qty[0][0]) if reserved_qty else 0.0 + + +def get_sre_reserved_qty_for_items_and_warehouses( + item_code_list: list, warehouse_list: list = None +) -> dict: + """Returns a dict like {("item_code", "warehouse"): "reserved_qty", ... }.""" + + if not item_code_list: + return {} + + sre = frappe.qb.DocType("Stock Reservation Entry") + query = ( + frappe.qb.from_(sre) + .select( + sre.item_code, + sre.warehouse, + Sum(sre.reserved_qty - sre.delivered_qty).as_("reserved_qty"), + ) + .where( + (sre.docstatus == 1) + & sre.item_code.isin(item_code_list) + & (sre.status.notin(["Delivered", "Cancelled"])) + ) + .groupby(sre.item_code, sre.warehouse) + ) + + if warehouse_list: + query = query.where(sre.warehouse.isin(warehouse_list)) + + data = query.run(as_dict=True) + + return {(d["item_code"], d["warehouse"]): d["reserved_qty"] for d in data} if data else {} + + +def get_sre_reserved_qty_details_for_voucher(voucher_type: str, voucher_no: str) -> dict: + """Returns a dict like {"voucher_detail_no": "reserved_qty", ... }.""" + + sre = frappe.qb.DocType("Stock Reservation Entry") + data = ( + frappe.qb.from_(sre) + .select( + sre.voucher_detail_no, + (Sum(sre.reserved_qty) - Sum(sre.delivered_qty)).as_("reserved_qty"), + ) + .where( + (sre.docstatus == 1) + & (sre.voucher_type == voucher_type) + & (sre.voucher_no == voucher_no) + & (sre.status.notin(["Delivered", "Cancelled"])) + ) + .groupby(sre.voucher_detail_no) + ).run(as_list=True) + + return frappe._dict(data) + + +def get_sre_reserved_warehouses_for_voucher( + voucher_type: str, voucher_no: str, voucher_detail_no: str = None +) -> list: + """Returns a list of warehouses where the stock is reserved for the provided voucher.""" + + sre = frappe.qb.DocType("Stock Reservation Entry") + query = ( + frappe.qb.from_(sre) + .select(sre.warehouse) + .distinct() + .where( + (sre.docstatus == 1) + & (sre.voucher_type == voucher_type) + & (sre.voucher_no == voucher_no) + & (sre.status.notin(["Delivered", "Cancelled"])) + ) + .orderby(sre.creation) + ) + + if voucher_detail_no: + query = query.where(sre.voucher_detail_no == voucher_detail_no) + + warehouses = query.run(as_list=True) + + return [d[0] for d in warehouses] if warehouses else [] + + +def get_sre_reserved_qty_for_voucher_detail_no( + voucher_type: str, voucher_no: str, voucher_detail_no: str, ignore_sre=None +) -> float: + """Returns `Reserved Qty` against the Voucher.""" + + sre = frappe.qb.DocType("Stock Reservation Entry") + query = ( + frappe.qb.from_(sre) + .select( + (Sum(sre.reserved_qty) - Sum(sre.delivered_qty)), + ) + .where( + (sre.docstatus == 1) + & (sre.voucher_type == voucher_type) + & (sre.voucher_no == voucher_no) + & (sre.voucher_detail_no == voucher_detail_no) + & (sre.status.notin(["Delivered", "Cancelled"])) + ) + ) + + if ignore_sre: + query = query.where(sre.name != ignore_sre) + + reserved_qty = query.run(as_list=True) + + return flt(reserved_qty[0][0]) + + +def get_sre_reserved_serial_nos_details( + item_code: str, warehouse: str, serial_nos: list = None +) -> dict: + """Returns a dict of `Serial No` reserved in Stock Reservation Entry. The dict is like {serial_no: sre_name, ...}""" + + sre = frappe.qb.DocType("Stock Reservation Entry") + sb_entry = frappe.qb.DocType("Serial and Batch Entry") + query = ( + frappe.qb.from_(sre) + .inner_join(sb_entry) + .on(sre.name == sb_entry.parent) + .select(sb_entry.serial_no, sre.name) + .where( + (sre.docstatus == 1) + & (sre.item_code == item_code) + & (sre.warehouse == warehouse) + & (sre.reserved_qty > sre.delivered_qty) + & (sre.status.notin(["Delivered", "Cancelled"])) + & (sre.reservation_based_on == "Serial and Batch") + ) + .orderby(sb_entry.creation) + ) + + if serial_nos: + query = query.where(sb_entry.serial_no.isin(serial_nos)) + + return frappe._dict(query.run()) + + +def get_sre_reserved_batch_nos_details( + item_code: str, warehouse: str, batch_nos: list = None +) -> dict: + """Returns a dict of `Batch Qty` reserved in Stock Reservation Entry. The dict is like {batch_no: qty, ...}""" + + sre = frappe.qb.DocType("Stock Reservation Entry") + sb_entry = frappe.qb.DocType("Serial and Batch Entry") + query = ( + frappe.qb.from_(sre) + .inner_join(sb_entry) + .on(sre.name == sb_entry.parent) + .select( + sb_entry.batch_no, + Sum(sb_entry.qty - sb_entry.delivered_qty), + ) + .where( + (sre.docstatus == 1) + & (sre.item_code == item_code) + & (sre.warehouse == warehouse) + & ((sre.reserved_qty - sre.delivered_qty) > 0) + & (sre.status.notin(["Delivered", "Cancelled"])) + & (sre.reservation_based_on == "Serial and Batch") + ) + .groupby(sb_entry.batch_no) + .orderby(sb_entry.creation) + ) + + if batch_nos: + query = query.where(sb_entry.batch_no.isin(batch_nos)) + + return frappe._dict(query.run()) + + +def get_sre_details_for_voucher(voucher_type: str, voucher_no: str) -> list[dict]: + """Returns a list of SREs for the provided voucher.""" + + sre = frappe.qb.DocType("Stock Reservation Entry") + return ( + frappe.qb.from_(sre) + .select( + sre.name, + sre.item_code, + sre.warehouse, + sre.voucher_type, + sre.voucher_no, + sre.voucher_detail_no, + (sre.reserved_qty - sre.delivered_qty).as_("reserved_qty"), + sre.has_serial_no, + sre.has_batch_no, + sre.reservation_based_on, + ) + .where( + (sre.docstatus == 1) + & (sre.voucher_type == voucher_type) + & (sre.voucher_no == voucher_no) + & (sre.reserved_qty > sre.delivered_qty) + & (sre.status.notin(["Delivered", "Cancelled"])) + ) + .orderby(sre.creation) + ).run(as_dict=True) + + +def get_serial_batch_entries_for_voucher(sre_name: str) -> list[dict]: + """Returns a list of `Serial and Batch Entries` for the provided voucher.""" + + sre = frappe.qb.DocType("Stock Reservation Entry") + sb_entry = frappe.qb.DocType("Serial and Batch Entry") + + return ( + frappe.qb.from_(sre) + .inner_join(sb_entry) + .on(sre.name == sb_entry.parent) + .select( + sb_entry.serial_no, + sb_entry.batch_no, + (sb_entry.qty - sb_entry.delivered_qty).as_("qty"), + ) + .where( + (sre.docstatus == 1) & (sre.name == sre_name) & (sre.status.notin(["Delivered", "Cancelled"])) + ) + .where(sb_entry.qty > sb_entry.delivered_qty) + .orderby(sb_entry.creation) + ).run(as_dict=True) + + +def get_ssb_bundle_for_voucher(sre: dict) -> object: + """Returns a new `Serial and Batch Bundle` against the provided SRE.""" + + sb_entries = get_serial_batch_entries_for_voucher(sre["name"]) + + if sb_entries: + bundle = frappe.new_doc("Serial and Batch Bundle") + bundle.type_of_transaction = "Outward" + bundle.voucher_type = "Delivery Note" + bundle.posting_date = nowdate() + bundle.posting_time = nowtime() + + for field in ("item_code", "warehouse", "has_serial_no", "has_batch_no"): + setattr(bundle, field, sre[field]) + + for sb_entry in sb_entries: + bundle.append("entries", sb_entry) + + bundle.save() + + return bundle.name + + +def has_reserved_stock(voucher_type: str, voucher_no: str, voucher_detail_no: str = None) -> bool: + """Returns True if there is any Stock Reservation Entry for the given voucher.""" + + if get_stock_reservation_entries_for_voucher( + voucher_type, voucher_no, voucher_detail_no, fields=["name"], ignore_status=True + ): + return True + + return False + + +def create_stock_reservation_entries_for_so_items( + sales_order: object, + items_details: list[dict] = None, + from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None, + notify=True, +) -> None: + """Creates Stock Reservation Entries for Sales Order Items.""" + + from erpnext.selling.doctype.sales_order.sales_order import get_unreserved_qty + + if not from_voucher_type and ( + sales_order.get("_action") == "submit" + and sales_order.set_warehouse + and cint(frappe.get_cached_value("Warehouse", sales_order.set_warehouse, "is_group")) + ): + return frappe.msgprint( + _("Stock cannot be reserved in the group warehouse {0}.").format( + frappe.bold(sales_order.set_warehouse) + ) + ) + + validate_stock_reservation_settings(sales_order) + + allow_partial_reservation = frappe.db.get_single_value( + "Stock Settings", "allow_partial_reservation" + ) + + items = [] + if items_details: + for item in items_details: + so_item = frappe.get_doc("Sales Order Item", item.get("sales_order_item")) + so_item.warehouse = item.get("warehouse") + so_item.qty_to_reserve = ( + flt(item.get("qty_to_reserve")) + if from_voucher_type in ["Pick List", "Purchase Receipt"] + else ( + flt(item.get("qty_to_reserve")) + * (flt(item.get("conversion_factor")) or flt(so_item.conversion_factor) or 1) + ) + ) + so_item.from_voucher_no = item.get("from_voucher_no") + so_item.from_voucher_detail_no = item.get("from_voucher_detail_no") + so_item.serial_and_batch_bundle = item.get("serial_and_batch_bundle") + + items.append(so_item) + + sre_count = 0 + reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", sales_order.name) + + for item in items if items_details else sales_order.get("items"): + # Skip if `Reserved Stock` is not checked for the item. + if not item.get("reserve_stock"): + continue + + # Stock should be reserved from the Pick List if has Picked Qty. + if from_voucher_type != "Pick List" and flt(item.picked_qty) > 0: + frappe.throw( + _("Row #{0}: Item {1} has been picked, please reserve stock from the Pick List.").format( + item.idx, frappe.bold(item.item_code) + ) + ) + + is_stock_item, has_serial_no, has_batch_no = frappe.get_cached_value( + "Item", item.item_code, ["is_stock_item", "has_serial_no", "has_batch_no"] + ) + + # Skip if Non-Stock Item. + if not is_stock_item: + if not from_voucher_type: + frappe.msgprint( + _("Row #{0}: Stock cannot be reserved for a non-stock Item {1}").format( + item.idx, frappe.bold(item.item_code) + ), + title=_("Stock Reservation"), + indicator="yellow", + ) + + item.db_set("reserve_stock", 0) + continue + + # Skip if Group Warehouse. + if frappe.get_cached_value("Warehouse", item.warehouse, "is_group"): + frappe.msgprint( + _("Row #{0}: Stock cannot be reserved in group warehouse {1}.").format( + item.idx, frappe.bold(item.warehouse) + ), + title=_("Stock Reservation"), + indicator="yellow", + ) + continue + + unreserved_qty = get_unreserved_qty(item, reserved_qty_details) + + # Stock is already reserved for the item, notify the user and skip the item. + if unreserved_qty <= 0: + if not from_voucher_type: + frappe.msgprint( + _("Row #{0}: Stock is already reserved for the Item {1}.").format( + item.idx, frappe.bold(item.item_code) + ), + title=_("Stock Reservation"), + indicator="yellow", + ) + + continue + + available_qty_to_reserve = get_available_qty_to_reserve(item.item_code, item.warehouse) + + # No stock available to reserve, notify the user and skip the item. + if available_qty_to_reserve <= 0: + frappe.msgprint( + _("Row #{0}: Stock not available to reserve for the Item {1} in Warehouse {2}.").format( + item.idx, frappe.bold(item.item_code), frappe.bold(item.warehouse) + ), + title=_("Stock Reservation"), + indicator="orange", + ) + continue + + # The quantity which can be reserved. + qty_to_be_reserved = min(unreserved_qty, available_qty_to_reserve) + + if hasattr(item, "qty_to_reserve"): + if item.qty_to_reserve <= 0: + frappe.msgprint( + _("Row #{0}: Quantity to reserve for the Item {1} should be greater than 0.").format( + item.idx, frappe.bold(item.item_code) + ), + title=_("Stock Reservation"), + indicator="orange", + ) + continue + else: + qty_to_be_reserved = min(qty_to_be_reserved, item.qty_to_reserve) + + # Partial Reservation + if qty_to_be_reserved < unreserved_qty: + if not from_voucher_type and ( + not item.get("qty_to_reserve") or qty_to_be_reserved < flt(item.get("qty_to_reserve")) + ): + msg = _("Row #{0}: Only {1} available to reserve for the Item {2}").format( + item.idx, + frappe.bold(str(qty_to_be_reserved / item.conversion_factor) + " " + item.uom), + frappe.bold(item.item_code), + ) + frappe.msgprint(msg, title=_("Stock Reservation"), indicator="orange") + + # Skip the item if `Partial Reservation` is disabled in the Stock Settings. + if not allow_partial_reservation: + if qty_to_be_reserved == flt(item.get("qty_to_reserve")): + msg = _("Enable Allow Partial Reservation in the Stock Settings to reserve partial stock.") + frappe.msgprint(msg, title=_("Partial Stock Reservation"), indicator="yellow") + + continue + + sre = frappe.new_doc("Stock Reservation Entry") + + sre.item_code = item.item_code + sre.warehouse = item.warehouse + sre.has_serial_no = has_serial_no + sre.has_batch_no = has_batch_no + sre.voucher_type = sales_order.doctype + sre.voucher_no = sales_order.name + sre.voucher_detail_no = item.name + sre.available_qty = available_qty_to_reserve + sre.voucher_qty = item.stock_qty + sre.reserved_qty = qty_to_be_reserved + sre.company = sales_order.company + sre.stock_uom = item.stock_uom + sre.project = sales_order.project + + if from_voucher_type: + sre.from_voucher_type = from_voucher_type + sre.from_voucher_no = item.from_voucher_no + sre.from_voucher_detail_no = item.from_voucher_detail_no + + if item.get("serial_and_batch_bundle"): + sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle) + sre.reservation_based_on = "Serial and Batch" + + index, picked_qty = 0, 0 + while index < len(sbb.entries) and picked_qty < qty_to_be_reserved: + entry = sbb.entries[index] + qty = 1 if has_serial_no else min(abs(entry.qty), qty_to_be_reserved - picked_qty) + + sre.append( + "sb_entries", + { + "serial_no": entry.serial_no, + "batch_no": entry.batch_no, + "qty": qty, + "warehouse": entry.warehouse, + }, + ) + + index += 1 + picked_qty += qty + + sre.save() + sre.submit() + + sre_count += 1 + + if sre_count and notify: + frappe.msgprint(_("Stock Reservation Entries Created"), alert=True, indicator="green") + + +def cancel_stock_reservation_entries( + voucher_type: str = None, + voucher_no: str = None, + voucher_detail_no: str = None, + from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None, + from_voucher_no: str = None, + from_voucher_detail_no: str = None, + sre_list: list = None, + notify: bool = True, +) -> None: + """Cancel Stock Reservation Entries.""" + + if not sre_list: + sre_list = {} + + if voucher_type and voucher_no: + sre_list = get_stock_reservation_entries_for_voucher( + voucher_type, voucher_no, voucher_detail_no, fields=["name"] + ) + elif from_voucher_type and from_voucher_no: + sre = frappe.qb.DocType("Stock Reservation Entry") + query = ( + frappe.qb.from_(sre) + .select(sre.name) + .where( + (sre.docstatus == 1) + & (sre.from_voucher_type == from_voucher_type) + & (sre.from_voucher_no == from_voucher_no) + & (sre.status.notin(["Delivered", "Cancelled"])) + ) + .orderby(sre.creation) + ) + + if from_voucher_detail_no: + query = query.where(sre.from_voucher_detail_no == from_voucher_detail_no) + + sre_list = query.run(as_dict=True) + + sre_list = [d.name for d in sre_list] + + if sre_list: + for sre in sre_list: + frappe.get_doc("Stock Reservation Entry", sre).cancel() + + if notify: + msg = _("Stock Reservation Entries Cancelled") + frappe.msgprint(msg, alert=True, indicator="red") + + +@frappe.whitelist() +def get_stock_reservation_entries_for_voucher( + voucher_type: str, + voucher_no: str, + voucher_detail_no: str = None, + fields: list[str] = None, + ignore_status: bool = False, +) -> list[dict]: + """Returns list of Stock Reservation Entries against a Voucher.""" + + if not fields or not isinstance(fields, list): + fields = [ + "name", + "item_code", + "warehouse", + "voucher_detail_no", + "reserved_qty", + "delivered_qty", + "stock_uom", + ] + + sre = frappe.qb.DocType("Stock Reservation Entry") + query = ( + frappe.qb.from_(sre) + .where( + (sre.docstatus == 1) & (sre.voucher_type == voucher_type) & (sre.voucher_no == voucher_no) + ) + .orderby(sre.creation) + ) + + for field in fields: + query = query.select(sre[field]) + + if voucher_detail_no: + query = query.where(sre.voucher_detail_no == voucher_detail_no) + + if ignore_status: + query = query.where(sre.status.notin(["Delivered", "Cancelled"])) + + return query.run(as_dict=True) diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py index e4f657ca7070..da958a8b0f12 100644 --- a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py +++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py @@ -5,7 +5,7 @@ import frappe from frappe import _ from frappe.query_builder import Field -from frappe.query_builder.functions import CombineDatetime, Min +from frappe.query_builder.functions import Min from frappe.utils import add_days, getdate, today import erpnext @@ -75,7 +75,7 @@ def get_data(report_filters): & (sle.company == report_filters.company) & (sle.is_cancelled == 0) ) - .orderby(CombineDatetime(sle.posting_date, sle.posting_time), sle.creation) + .orderby(sle.posting_datetime, sle.creation) ).run(as_dict=True) for d in data: diff --git a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py index 9e75201bd141..dd79e7fcaf58 100644 --- a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py +++ b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py @@ -213,13 +213,11 @@ def get_stock_ledger_entries(filters, items): query = ( frappe.qb.from_(sle) - .force_index("posting_sort_index") .left_join(sle2) .on( (sle.item_code == sle2.item_code) & (sle.warehouse == sle2.warehouse) - & (sle.posting_date < sle2.posting_date) - & (sle.posting_time < sle2.posting_time) + & (sle.posting_datetime < sle2.posting_datetime) & (sle.name < sle2.name) ) .select(sle.item_code, sle.warehouse, sle.qty_after_transaction, sle.company) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 32a2b302d7b2..6b0bbf3f44dc 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -8,7 +8,7 @@ import frappe from frappe import _ from frappe.query_builder import Order -from frappe.query_builder.functions import Coalesce, CombineDatetime +from frappe.query_builder.functions import Coalesce from frappe.utils import add_days, cint, date_diff, flt, getdate from frappe.utils.nestedset import get_descendants_of @@ -283,7 +283,7 @@ def prepare_stock_ledger_entries(self): item_table.item_name, ) .where((sle.docstatus < 2) & (sle.is_cancelled == 0)) - .orderby(CombineDatetime(sle.posting_date, sle.posting_time)) + .orderby(sle.posting_datetime) .orderby(sle.creation) .orderby(sle.actual_qty) ) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index eeef39641b01..21b90c4b026e 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -276,7 +276,7 @@ def get_stock_ledger_entries(filters, items): frappe.qb.from_(sle) .select( sle.item_code, - CombineDatetime(sle.posting_date, sle.posting_time).as_("date"), + sle.posting_datetime.as_("date"), sle.warehouse, sle.posting_date, sle.posting_time, diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index ef1b0cda4ff7..ee06dcecd522 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -7,13 +7,30 @@ import frappe from frappe import _ from frappe.model.meta import get_field_precision +<<<<<<< HEAD from frappe.query_builder.functions import CombineDatetime, Sum from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate +======= +from frappe.query_builder.functions import Sum +from frappe.utils import ( + add_to_date, + cint, + cstr, + flt, + get_link_to_form, + getdate, + now, + nowdate, + nowtime, + parse_json, +) +>>>>>>> d80ca523a4 (perf: new column posting datetime in SLE to optimize stock ledger related queries) import erpnext from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions from erpnext.stock.utils import ( + get_combine_datetime, get_incoming_outgoing_rate_for_cancel, get_incoming_rate, get_or_make_bin, @@ -68,7 +85,11 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher) args = sle_doc.as_dict() +<<<<<<< HEAD args["allow_zero_valuation_rate"] = sle.get("allow_zero_valuation_rate") or False +======= + args["posting_datetime"] = get_combine_datetime(args.posting_date, args.posting_time) +>>>>>>> d80ca523a4 (perf: new column posting datetime in SLE to optimize stock ledger related queries) if sle.get("voucher_type") == "Stock Reconciliation": # preserve previous_qty_after_transaction for qty reposting @@ -431,12 +452,14 @@ def process_sle_against_current_timestamp(self): self.process_sle(sle) def get_sle_against_current_voucher(self): - self.args["time_format"] = "%H:%i:%s" + self.args["posting_datetime"] = get_combine_datetime( + self.args.posting_date, self.args.posting_time + ) return frappe.db.sql( """ select - *, timestamp(posting_date, posting_time) as "timestamp" + *, posting_datetime as "timestamp" from `tabStock Ledger Entry` where @@ -444,11 +467,10 @@ def get_sle_against_current_voucher(self): and warehouse = %(warehouse)s and is_cancelled = 0 and ( - posting_date = %(posting_date)s and - time_format(posting_time, %(time_format)s) = time_format(%(posting_time)s, %(time_format)s) + posting_datetime = %(posting_datetime)s ) order by - creation ASC + posting_datetime ASC, creation ASC for update """, self.args, @@ -1188,9 +1210,14 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc args["time_format"] = "%H:%i:%s" if not args.get("posting_date"): - args["posting_date"] = "1900-01-01" - if not args.get("posting_time"): - args["posting_time"] = "00:00" + args["posting_datetime"] = "1900-01-01 00:00:00" + + if not args.get("posting_datetime"): + args["posting_datetime"] = get_combine_datetime(args["posting_date"], args["posting_time"]) + + if operator == "<=": + # Add 1 second to handle millisecond for less than and equal to condition + args["posting_datetime"] = add_to_date(args["posting_datetime"], seconds=1) voucher_condition = "" if exclude_current_voucher: @@ -1199,23 +1226,20 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc sle = frappe.db.sql( """ - select *, timestamp(posting_date, posting_time) as "timestamp" + select *, posting_datetime as "timestamp" from `tabStock Ledger Entry` where item_code = %(item_code)s and warehouse = %(warehouse)s and is_cancelled = 0 {voucher_condition} and ( - posting_date < %(posting_date)s or - ( - posting_date = %(posting_date)s and - time_format(posting_time, %(time_format)s) {operator} time_format(%(posting_time)s, %(time_format)s) - ) + posting_datetime {operator} %(posting_datetime)s ) - order by timestamp(posting_date, posting_time) desc, creation desc + order by posting_datetime desc, creation desc limit 1 for update""".format( - operator=operator, voucher_condition=voucher_condition + operator=operator, + voucher_condition=voucher_condition, ), args, as_dict=1, @@ -1256,9 +1280,7 @@ def get_stock_ledger_entries( extra_cond=None, ): """get stock ledger entries filtered by specific posting datetime conditions""" - conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format( - operator - ) + conditions = " and posting_datetime {0} %(posting_datetime)s".format(operator) if previous_sle.get("warehouse"): conditions += " and warehouse = %(warehouse)s" elif previous_sle.get("warehouse_condition"): @@ -1284,9 +1306,11 @@ def get_stock_ledger_entries( ) if not previous_sle.get("posting_date"): - previous_sle["posting_date"] = "1900-01-01" - if not previous_sle.get("posting_time"): - previous_sle["posting_time"] = "00:00" + previous_sle["posting_datetime"] = "1900-01-01 00:00:00" + else: + previous_sle["posting_datetime"] = get_combine_datetime( + previous_sle["posting_date"], previous_sle["posting_time"] + ) if operator in (">", "<=") and previous_sle.get("name"): conditions += " and name!=%(name)s" @@ -1299,12 +1323,12 @@ def get_stock_ledger_entries( return frappe.db.sql( """ - select *, timestamp(posting_date, posting_time) as "timestamp" + select *, posting_datetime as "timestamp" from `tabStock Ledger Entry` where item_code = %%(item_code)s and is_cancelled = 0 %(conditions)s - order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s + order by posting_datetime %(order)s, creation %(order)s %(limit)s %(for_update)s""" % { "conditions": conditions, @@ -1330,7 +1354,7 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None): "posting_date", "posting_time", "voucher_detail_no", - "timestamp(posting_date, posting_time) as timestamp", + "posting_datetime as timestamp", ], as_dict=1, ) @@ -1342,13 +1366,10 @@ def get_batch_incoming_rate( sle = frappe.qb.DocType("Stock Ledger Entry") - timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime( - posting_date, posting_time - ) + timestamp_condition = sle.posting_datetime < get_combine_datetime(posting_date, posting_time) if creation: timestamp_condition |= ( - CombineDatetime(sle.posting_date, sle.posting_time) - == CombineDatetime(posting_date, posting_time) + sle.posting_datetime == get_combine_datetime(posting_date, posting_time) ) & (sle.creation < creation) batch_details = ( @@ -1401,6 +1422,7 @@ def get_valuation_rate( ) # Get valuation rate from last sle for the same item and warehouse +<<<<<<< HEAD if not last_valuation_rate or last_valuation_rate[0][0] is None: last_valuation_rate = frappe.db.sql( """select valuation_rate @@ -1416,6 +1438,20 @@ def get_valuation_rate( ) if last_valuation_rate: +======= + if last_valuation_rate := frappe.db.sql( + """select valuation_rate + from `tabStock Ledger Entry` force index (item_warehouse) + where + item_code = %s + AND warehouse = %s + AND valuation_rate >= 0 + AND is_cancelled = 0 + AND NOT (voucher_no = %s AND voucher_type = %s) + order by posting_datetime desc, name desc limit 1""", + (item_code, warehouse, voucher_no, voucher_type), + ): +>>>>>>> d80ca523a4 (perf: new column posting datetime in SLE to optimize stock ledger related queries) return flt(last_valuation_rate[0][0]) # If negative stock allowed, and item delivered without any incoming entry, @@ -1473,6 +1509,7 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): qty_shift = args.actual_qty args["time_format"] = "%H:%i:%s" + args["posting_datetime"] = get_combine_datetime(args["posting_date"], args["posting_time"]) # find difference/shift in qty caused by stock reconciliation if args.voucher_type == "Stock Reconciliation": @@ -1482,8 +1519,6 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): next_stock_reco_detail = get_next_stock_reco(args) if next_stock_reco_detail: detail = next_stock_reco_detail[0] - - # add condition to update SLEs before this date & time datetime_limit_condition = get_datetime_limit_condition(detail) frappe.db.sql( @@ -1496,13 +1531,9 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): and voucher_no != %(voucher_no)s and is_cancelled = 0 and ( - posting_date > %(posting_date)s or - ( - posting_date = %(posting_date)s and - time_format(posting_time, %(time_format)s) > time_format(%(posting_time)s, %(time_format)s) - ) + posting_datetime > %(posting_datetime)s ) - {datetime_limit_condition} + {datetime_limit_condition} """, args, ) @@ -1557,20 +1588,11 @@ def get_next_stock_reco(kwargs): & (sle.voucher_no != kwargs.get("voucher_no")) & (sle.is_cancelled == 0) & ( - ( - CombineDatetime(sle.posting_date, sle.posting_time) - > CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time")) - ) - | ( - ( - CombineDatetime(sle.posting_date, sle.posting_time) - == CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time")) - ) - & (sle.creation > kwargs.get("creation")) - ) + sle.posting_datetime + >= get_combine_datetime(kwargs.get("posting_date"), kwargs.get("posting_time")) ) ) - .orderby(CombineDatetime(sle.posting_date, sle.posting_time)) + .orderby(sle.posting_datetime) .orderby(sle.creation) .limit(1) ) @@ -1582,11 +1604,13 @@ def get_next_stock_reco(kwargs): def get_datetime_limit_condition(detail): + posting_datetime = get_combine_datetime(detail.posting_date, detail.posting_time) + return f""" and - (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}') + (posting_datetime < '{posting_datetime}' or ( - timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}') + posting_datetime = '{posting_datetime}' and creation < '{detail.creation}' ) )""" @@ -1648,6 +1672,7 @@ def is_negative_with_precision(neg_sle, is_batch=False): return qty_deficit < 0 and abs(qty_deficit) > 0.0001 +<<<<<<< HEAD def get_future_sle_with_negative_qty(sle): SLE = frappe.qb.DocType("Stock Ledger Entry") query = ( @@ -1668,6 +1693,27 @@ def get_future_sle_with_negative_qty(sle): ) .orderby(CombineDatetime(SLE.posting_date, SLE.posting_time)) .limit(1) +======= +def get_future_sle_with_negative_qty(args): + return frappe.db.sql( + """ + select + qty_after_transaction, posting_date, posting_time, + voucher_type, voucher_no + from `tabStock Ledger Entry` + where + item_code = %(item_code)s + and warehouse = %(warehouse)s + and voucher_no != %(voucher_no)s + and posting_datetime >= %(posting_datetime)s + and is_cancelled = 0 + and qty_after_transaction < 0 + order by posting_datetime asc + limit 1 + """, + args, + as_dict=1, +>>>>>>> d80ca523a4 (perf: new column posting datetime in SLE to optimize stock ledger related queries) ) if sle.voucher_type == "Stock Reconciliation" and sle.batch_no: @@ -1681,20 +1727,20 @@ def get_future_sle_with_negative_batch_qty(args): """ with batch_ledger as ( select - posting_date, posting_time, voucher_type, voucher_no, - sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total + posting_date, posting_time, posting_datetime, voucher_type, voucher_no, + sum(actual_qty) over (order by posting_datetime, creation) as cumulative_total from `tabStock Ledger Entry` where item_code = %(item_code)s and warehouse = %(warehouse)s and batch_no=%(batch_no)s and is_cancelled = 0 - order by posting_date, posting_time, creation + order by posting_datetime, creation ) select * from batch_ledger where cumulative_total < 0.0 - and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s) + and posting_datetime >= %(posting_datetime)s limit 1 """, args, @@ -1746,6 +1792,7 @@ def is_internal_transfer(sle): def get_stock_value_difference(item_code, warehouse, posting_date, posting_time, voucher_no=None): table = frappe.qb.DocType("Stock Ledger Entry") + posting_datetime = get_combine_datetime(posting_date, posting_time) query = ( frappe.qb.from_(table) @@ -1754,10 +1801,7 @@ def get_stock_value_difference(item_code, warehouse, posting_date, posting_time, (table.is_cancelled == 0) & (table.item_code == item_code) & (table.warehouse == warehouse) - & ( - (table.posting_date < posting_date) - | ((table.posting_date == posting_date) & (table.posting_time <= posting_time)) - ) + & (table.posting_datetime <= posting_datetime) ) ) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 2b57a1be8fad..c6ee01d3df15 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -8,7 +8,7 @@ import frappe from frappe import _ from frappe.query_builder.functions import CombineDatetime, IfNull, Sum -from frappe.utils import cstr, flt, get_link_to_form, nowdate, nowtime +from frappe.utils import cstr, flt, get_link_to_form, get_time, getdate, nowdate, nowtime import erpnext from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses @@ -619,3 +619,18 @@ def _update_item_info(scan_result: Dict[str, Optional[str]]) -> Dict[str, Option ): scan_result.update(item_info) return scan_result + + +def get_combine_datetime(posting_date, posting_time): + import datetime + + if isinstance(posting_date, str): + posting_date = getdate(posting_date) + + if isinstance(posting_time, str): + posting_time = get_time(posting_time) + + if isinstance(posting_time, datetime.timedelta): + posting_time = (datetime.datetime.min + posting_time).time() + + return datetime.datetime.combine(posting_date, posting_time) From 1767dada8df1804a0404e930206efb08a24e6367 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 15 Feb 2024 18:08:14 +0530 Subject: [PATCH 37/94] test: test cases to test clash timestamp entries (cherry picked from commit f04676aaed69d669e43099077c85d93fedbf7e07) --- .../test_stock_ledger_entry.py | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index 44ebcda1ed20..570e280a9344 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -2,6 +2,7 @@ # See license.txt import json +import time from uuid import uuid4 import frappe @@ -1143,6 +1144,89 @@ def test_timestamp_clash(self): except Exception as e: self.fail("Double processing of qty for clashing timestamp.") + def test_previous_sle_with_clashed_timestamp(self): + + item = make_item().name + warehouse = "_Test Warehouse - _TC" + + reciept1 = make_stock_entry( + item_code=item, + to_warehouse=warehouse, + qty=100, + rate=10, + posting_date="2021-01-01", + posting_time="02:00:00", + ) + + time.sleep(3) + + reciept2 = make_stock_entry( + item_code=item, + to_warehouse=warehouse, + qty=5, + posting_date="2021-01-01", + rate=10, + posting_time="02:00:00.1234", + ) + + sle = frappe.get_all( + "Stock Ledger Entry", + filters={"voucher_no": reciept1.name}, + fields=["qty_after_transaction", "actual_qty"], + ) + self.assertEqual(sle[0].qty_after_transaction, 100) + self.assertEqual(sle[0].actual_qty, 100) + + sle = frappe.get_all( + "Stock Ledger Entry", + filters={"voucher_no": reciept2.name}, + fields=["qty_after_transaction", "actual_qty"], + ) + self.assertEqual(sle[0].qty_after_transaction, 105) + self.assertEqual(sle[0].actual_qty, 5) + + def test_backdated_sle_with_same_timestamp(self): + + item = make_item().name + warehouse = "_Test Warehouse - _TC" + + reciept1 = make_stock_entry( + item_code=item, + to_warehouse=warehouse, + qty=5, + posting_date="2021-01-01", + rate=10, + posting_time="02:00:00.1234", + ) + + time.sleep(3) + + # backdated entry with same timestamp but different ms part + reciept2 = make_stock_entry( + item_code=item, + to_warehouse=warehouse, + qty=100, + rate=10, + posting_date="2021-01-01", + posting_time="02:00:00", + ) + + sle = frappe.get_all( + "Stock Ledger Entry", + filters={"voucher_no": reciept1.name}, + fields=["qty_after_transaction", "actual_qty"], + ) + self.assertEqual(sle[0].qty_after_transaction, 105) + self.assertEqual(sle[0].actual_qty, 5) + + sle = frappe.get_all( + "Stock Ledger Entry", + filters={"voucher_no": reciept2.name}, + fields=["qty_after_transaction", "actual_qty"], + ) + self.assertEqual(sle[0].qty_after_transaction, 100) + self.assertEqual(sle[0].actual_qty, 100) + @change_settings("System Settings", {"float_precision": 3, "currency_precision": 2}) def test_transfer_invariants(self): """Extact stock value should be transferred.""" From ce7a53f810fae8fe10d81ab416845daaaa9d8089 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 21 Feb 2024 17:32:02 +0530 Subject: [PATCH 38/94] chore: remove microsecond from posting_datetime (cherry picked from commit a73ba2c0d26b9d27bb5a75bc6c9739e49035f266) # Conflicts: # erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py --- .../doctype/stock_entry/test_stock_entry.py | 10 +- .../test_stock_ledger_entry.py | 4 +- erpnext/stock/stock_ledger.py | 9 +- erpnext/stock/utils.py | 2 +- .../test_subcontracting_receipt.py | 253 ++++++++++++++++++ 5 files changed, 261 insertions(+), 17 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 1d7e4da26d53..771dae538645 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -1671,24 +1671,22 @@ def test_negative_stock_reco(self): item_code = "Test Negative Item - 001" item_doc = create_item(item_code=item_code, is_stock_item=1, valuation_rate=10) - make_stock_entry( + se1 = make_stock_entry( item_code=item_code, posting_date=add_days(today(), -3), posting_time="00:00:00", - purpose="Material Receipt", + target="_Test Warehouse - _TC", qty=10, to_warehouse="_Test Warehouse - _TC", - do_not_save=True, ) - make_stock_entry( + se2 = make_stock_entry( item_code=item_code, posting_date=today(), posting_time="00:00:00", - purpose="Material Receipt", + source="_Test Warehouse - _TC", qty=8, from_warehouse="_Test Warehouse - _TC", - do_not_save=True, ) sr_doc = create_stock_reconciliation( diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index 570e280a9344..6154910c2f11 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -1216,7 +1216,7 @@ def test_backdated_sle_with_same_timestamp(self): filters={"voucher_no": reciept1.name}, fields=["qty_after_transaction", "actual_qty"], ) - self.assertEqual(sle[0].qty_after_transaction, 105) + self.assertEqual(sle[0].qty_after_transaction, 5) self.assertEqual(sle[0].actual_qty, 5) sle = frappe.get_all( @@ -1224,7 +1224,7 @@ def test_backdated_sle_with_same_timestamp(self): filters={"voucher_no": reciept2.name}, fields=["qty_after_transaction", "actual_qty"], ) - self.assertEqual(sle[0].qty_after_transaction, 100) + self.assertEqual(sle[0].qty_after_transaction, 105) self.assertEqual(sle[0].actual_qty, 100) @change_settings("System Settings", {"float_precision": 3, "currency_precision": 2}) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index ee06dcecd522..28ed449c451c 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -13,7 +13,6 @@ ======= from frappe.query_builder.functions import Sum from frappe.utils import ( - add_to_date, cint, cstr, flt, @@ -470,7 +469,7 @@ def get_sle_against_current_voucher(self): posting_datetime = %(posting_datetime)s ) order by - posting_datetime ASC, creation ASC + creation ASC for update """, self.args, @@ -1208,17 +1207,12 @@ def update_bin(self): def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False): """get stock ledger entries filtered by specific posting datetime conditions""" - args["time_format"] = "%H:%i:%s" if not args.get("posting_date"): args["posting_datetime"] = "1900-01-01 00:00:00" if not args.get("posting_datetime"): args["posting_datetime"] = get_combine_datetime(args["posting_date"], args["posting_time"]) - if operator == "<=": - # Add 1 second to handle millisecond for less than and equal to condition - args["posting_datetime"] = add_to_date(args["posting_datetime"], seconds=1) - voucher_condition = "" if exclude_current_voucher: voucher_no = args.get("voucher_no") @@ -1508,7 +1502,6 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): datetime_limit_condition = "" qty_shift = args.actual_qty - args["time_format"] = "%H:%i:%s" args["posting_datetime"] = get_combine_datetime(args["posting_date"], args["posting_time"]) # find difference/shift in qty caused by stock reconciliation diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index c6ee01d3df15..0c3e15ac487d 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -633,4 +633,4 @@ def get_combine_datetime(posting_date, posting_time): if isinstance(posting_time, datetime.timedelta): posting_time = (datetime.datetime.min + posting_time).time() - return datetime.datetime.combine(posting_date, posting_time) + return datetime.datetime.combine(posting_date, posting_time).replace(microsecond=0) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index b05ed755c7f9..0862970c1194 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -594,6 +594,259 @@ def test_supplied_items_cost_after_reposting(self): self.assertNotEqual(scr.supplied_items[0].rate, prev_cost) self.assertEqual(scr.supplied_items[0].rate, sr.items[0].valuation_rate) +<<<<<<< HEAD +======= + def test_subcontracting_receipt_for_batch_raw_materials_without_material_transfer(self): + set_backflush_based_on("BOM") + + fg_item = make_item(properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name + rm_item1 = make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BNGS-.####", + } + ).name + + bom = make_bom(item=fg_item, raw_materials=[rm_item1]) + + rm_batch_no = None + for row in bom.items: + se = make_stock_entry( + item_code=row.item_code, + qty=1, + target="_Test Warehouse 1 - _TC", + rate=300, + ) + + se.reload() + rm_batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 1, + "rate": 100, + "fg_item": fg_item, + "fg_item_qty": 1, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + scr = make_subcontracting_receipt(sco.name) + scr.save() + scr.reload() + + bundle_doc = make_serial_batch_bundle( + { + "item_code": scr.supplied_items[0].rm_item_code, + "warehouse": "_Test Warehouse 1 - _TC", + "voucher_type": "Subcontracting Receipt", + "posting_date": today(), + "posting_time": nowtime(), + "qty": -1, + "batches": frappe._dict({rm_batch_no: 1}), + "type_of_transaction": "Outward", + "do_not_submit": True, + } + ) + + scr.supplied_items[0].serial_and_batch_bundle = bundle_doc.name + scr.submit() + scr.reload() + + batch_no = get_batch_from_bundle(scr.supplied_items[0].serial_and_batch_bundle) + self.assertEqual(batch_no, rm_batch_no) + self.assertEqual(scr.items[0].rm_cost_per_qty, 300) + self.assertEqual(scr.items[0].service_cost_per_qty, 100) + + def test_subcontracting_receipt_valuation_with_auto_created_serial_batch_bundle(self): + set_backflush_based_on("BOM") + + fg_item = make_item(properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name + rm_item1 = make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BNGS-.####", + } + ).name + + rm_item2 = make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "has_serial_no": 1, + "create_new_batch": 1, + "batch_number_series": "BNGS-.####", + "serial_no_series": "BNSS-.####", + } + ).name + + rm_item3 = make_item( + properties={ + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "BSSSS-.####", + } + ).name + + bom = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2, rm_item3]) + + rm_batch_no = None + for row in bom.items: + make_stock_entry( + item_code=row.item_code, + qty=1, + target="_Test Warehouse 1 - _TC", + rate=300, + ) + + make_stock_entry( + item_code=row.item_code, + qty=1, + target="_Test Warehouse 1 - _TC", + rate=400, + ) + + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 1, + "rate": 100, + "fg_item": fg_item, + "fg_item_qty": 1, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + + frappe.db.set_single_value( + "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 1 + ) + scr = make_subcontracting_receipt(sco.name) + scr.save() + scr.submit() + scr.reload() + + for row in scr.supplied_items: + self.assertEqual(row.rate, 300.00) + self.assertTrue(row.serial_and_batch_bundle) + auto_created_serial_batch = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": scr.name, "voucher_detail_no": row.name}, + "auto_created_serial_and_batch_bundle", + ) + + self.assertTrue(auto_created_serial_batch) + + self.assertEqual(scr.items[0].rm_cost_per_qty, 900) + self.assertEqual(scr.items[0].service_cost_per_qty, 100) + frappe.db.set_single_value( + "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 0 + ) + + def test_subcontracting_receipt_valuation_for_fg_with_auto_created_serial_batch_bundle(self): + set_backflush_based_on("BOM") + + fg_item = make_item( + properties={ + "is_stock_item": 1, + "is_sub_contracted_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BSSNGS-.####", + } + ).name + + rm_item1 = make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BNGS-.####", + } + ).name + + rm_item2 = make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "has_serial_no": 1, + "create_new_batch": 1, + "batch_number_series": "BNGS-.####", + "serial_no_series": "BNSS-.####", + } + ).name + + rm_item3 = make_item( + properties={ + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "BSSSS-.####", + } + ).name + + bom = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2, rm_item3]) + + rm_batch_no = None + for row in bom.items: + make_stock_entry( + item_code=row.item_code, + qty=1, + target="_Test Warehouse 1 - _TC", + rate=300, + ) + + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 1, + "rate": 100, + "fg_item": fg_item, + "fg_item_qty": 1, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + + frappe.db.set_single_value( + "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 1 + ) + scr = make_subcontracting_receipt(sco.name) + scr.save() + scr.submit() + scr.reload() + + for row in scr.supplied_items: + self.assertEqual(row.rate, 300.00) + self.assertTrue(row.serial_and_batch_bundle) + auto_created_serial_batch = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": scr.name, "voucher_detail_no": row.name}, + "auto_created_serial_and_batch_bundle", + ) + + self.assertTrue(auto_created_serial_batch) + + self.assertEqual(scr.items[0].rm_cost_per_qty, 900) + self.assertEqual(scr.items[0].service_cost_per_qty, 100) + self.assertEqual(scr.items[0].rate, 1000) + valuation_rate = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": scr.name, "voucher_detail_no": scr.items[0].name}, + "valuation_rate", + ) + + self.assertEqual(flt(valuation_rate), flt(1000)) + + frappe.db.set_single_value( + "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 0 + ) + +>>>>>>> a73ba2c0d2 (chore: remove microsecond from posting_datetime) def test_subcontracting_receipt_raw_material_rate(self): from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom From 717442c01fa56c42e3d8ab8da513ff82c2fa2d74 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 26 Feb 2024 13:03:51 +0530 Subject: [PATCH 39/94] chore: fix conflicts --- .../stock/doctype/stock_entry/stock_entry.py | 138 ------------------ 1 file changed, 138 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 408aeb12be17..ca31a9a3d31c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1721,22 +1721,9 @@ def add_batchwise_finished_good(self, data, args, item): if qty <= 0: break -<<<<<<< HEAD fg_qty = batch_qty if batch_qty >= qty: fg_qty = qty -======= - id = create_serial_and_batch_bundle( - self, - row, - frappe._dict( - { - "item_code": self.pro_doc.production_item, - "warehouse": args.get("to_warehouse"), - } - ), - ) ->>>>>>> d80ca523a4 (perf: new column posting datetime in SLE to optimize stock ledger related queries) qty -= batch_qty args["qty"] = fg_qty @@ -1982,11 +1969,7 @@ def update_item_in_stock_entry_detail(self, row, item, qty) -> None: "to_warehouse": "", "qty": qty, "item_name": item.item_name, -<<<<<<< HEAD "batch_no": item.batch_no, -======= - "serial_and_batch_bundle": create_serial_and_batch_bundle(self, row, item, "Outward"), ->>>>>>> d80ca523a4 (perf: new column posting datetime in SLE to optimize stock ledger related queries) "description": item.description, "stock_uom": item.stock_uom, "expense_account": item.expense_account, @@ -2394,39 +2377,6 @@ def set_material_request_transfer_status(self, status): frappe.db.set_value("Material Request", material_request, "transfer_status", status) def set_serial_no_batch_for_finished_good(self): -<<<<<<< HEAD -======= - if not ( - (self.pro_doc.has_serial_no or self.pro_doc.has_batch_no) - and frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order") - ): - return - - for d in self.items: - if ( - d.is_finished_item - and d.item_code == self.pro_doc.production_item - and not d.serial_and_batch_bundle - ): - serial_nos = self.get_available_serial_nos() - if serial_nos: - row = frappe._dict({"serial_nos": serial_nos[0 : cint(d.qty)]}) - - id = create_serial_and_batch_bundle( - self, - row, - frappe._dict( - { - "item_code": d.item_code, - "warehouse": d.t_warehouse, - } - ), - ) - - d.serial_and_batch_bundle = id - - def get_available_serial_nos(self) -> List[str]: ->>>>>>> d80ca523a4 (perf: new column posting datetime in SLE to optimize stock ledger related queries) serial_nos = [] if self.pro_doc.serial_no: serial_nos = self.get_serial_nos_for_fg() or [] @@ -2905,91 +2855,3 @@ def get_stock_entry_data(work_order): ) .orderby(stock_entry.creation, stock_entry_detail.item_code, stock_entry_detail.idx) ).run(as_dict=1) -<<<<<<< HEAD -======= - - if not data: - return [] - - voucher_nos = [row.get("name") for row in data if row.get("name")] - if voucher_nos: - bundle_data = get_voucher_wise_serial_batch_from_bundle(voucher_no=voucher_nos) - for row in data: - key = (row.item_code, row.warehouse, row.name) - if row.purpose != "Material Transfer for Manufacture": - key = (row.item_code, row.s_warehouse, row.name) - - if bundle_data.get(key): - row.update(bundle_data.get(key)) - - return data - - -def create_serial_and_batch_bundle(parent_doc, row, child, type_of_transaction=None): - item_details = frappe.get_cached_value( - "Item", child.item_code, ["has_serial_no", "has_batch_no"], as_dict=1 - ) - - if not (item_details.has_serial_no or item_details.has_batch_no): - return - - if not type_of_transaction: - type_of_transaction = "Inward" - - doc = frappe.get_doc( - { - "doctype": "Serial and Batch Bundle", - "voucher_type": "Stock Entry", - "item_code": child.item_code, - "warehouse": child.warehouse, - "type_of_transaction": type_of_transaction, - "posting_date": parent_doc.posting_date, - "posting_time": parent_doc.posting_time, - } - ) - - if row.serial_nos and row.batches_to_be_consume: - doc.has_serial_no = 1 - doc.has_batch_no = 1 - batchwise_serial_nos = get_batchwise_serial_nos(child.item_code, row) - for batch_no, qty in row.batches_to_be_consume.items(): - - while qty > 0: - qty -= 1 - doc.append( - "entries", - { - "batch_no": batch_no, - "serial_no": batchwise_serial_nos.get(batch_no).pop(0), - "warehouse": row.warehouse, - "qty": -1, - }, - ) - - elif row.serial_nos: - doc.has_serial_no = 1 - for serial_no in row.serial_nos: - doc.append("entries", {"serial_no": serial_no, "warehouse": row.warehouse, "qty": -1}) - - elif row.batches_to_be_consume: - doc.has_batch_no = 1 - for batch_no, qty in row.batches_to_be_consume.items(): - doc.append("entries", {"batch_no": batch_no, "warehouse": row.warehouse, "qty": qty * -1}) - - return doc.insert(ignore_permissions=True).name - - -def get_batchwise_serial_nos(item_code, row): - batchwise_serial_nos = {} - - for batch_no in row.batches_to_be_consume: - serial_nos = frappe.get_all( - "Serial No", - filters={"item_code": item_code, "batch_no": batch_no, "name": ("in", row.serial_nos)}, - ) - - if serial_nos: - batchwise_serial_nos[batch_no] = sorted([serial_no.name for serial_no in serial_nos]) - - return batchwise_serial_nos ->>>>>>> d80ca523a4 (perf: new column posting datetime in SLE to optimize stock ledger related queries) From c147ec168fddf81ca76782f2e6cefd2540ad1a41 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 26 Feb 2024 13:06:26 +0530 Subject: [PATCH 40/94] chore: fix conflicts --- .../stock_ledger_entry/stock_ledger_entry.py | 44 ------------------- 1 file changed, 44 deletions(-) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 8864ef4b2f29..da4f2c9db808 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -28,50 +28,6 @@ class BackDatedStockTransaction(frappe.ValidationError): class StockLedgerEntry(Document): -<<<<<<< HEAD -======= - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - actual_qty: DF.Float - auto_created_serial_and_batch_bundle: DF.Check - batch_no: DF.Data | None - company: DF.Link | None - dependant_sle_voucher_detail_no: DF.Data | None - fiscal_year: DF.Data | None - has_batch_no: DF.Check - has_serial_no: DF.Check - incoming_rate: DF.Currency - is_adjustment_entry: DF.Check - is_cancelled: DF.Check - item_code: DF.Link | None - outgoing_rate: DF.Currency - posting_date: DF.Date | None - posting_datetime: DF.Datetime | None - posting_time: DF.Time | None - project: DF.Link | None - qty_after_transaction: DF.Float - recalculate_rate: DF.Check - serial_and_batch_bundle: DF.Link | None - serial_no: DF.LongText | None - stock_queue: DF.Text | None - stock_uom: DF.Link | None - stock_value: DF.Currency - stock_value_difference: DF.Currency - to_rename: DF.Check - valuation_rate: DF.Currency - voucher_detail_no: DF.Data | None - voucher_no: DF.DynamicLink | None - voucher_type: DF.Link | None - warehouse: DF.Link | None - # end: auto-generated types - ->>>>>>> d80ca523a4 (perf: new column posting datetime in SLE to optimize stock ledger related queries) def autoname(self): """ Temporarily name doc for fast insertion From 1c7128e77bcaa258e54cf1047c904bfabbace9ae Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 26 Feb 2024 13:16:08 +0530 Subject: [PATCH 41/94] chore: fix conflicts --- erpnext/stock/stock_ledger.py | 66 +++-------------------------------- 1 file changed, 4 insertions(+), 62 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 28ed449c451c..ce1ffa52edee 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -7,23 +7,8 @@ import frappe from frappe import _ from frappe.model.meta import get_field_precision -<<<<<<< HEAD from frappe.query_builder.functions import CombineDatetime, Sum -from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate -======= -from frappe.query_builder.functions import Sum -from frappe.utils import ( - cint, - cstr, - flt, - get_link_to_form, - getdate, - now, - nowdate, - nowtime, - parse_json, -) ->>>>>>> d80ca523a4 (perf: new column posting datetime in SLE to optimize stock ledger related queries) +from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate, nowtime, parse_json import erpnext from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty @@ -84,11 +69,8 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher) args = sle_doc.as_dict() -<<<<<<< HEAD args["allow_zero_valuation_rate"] = sle.get("allow_zero_valuation_rate") or False -======= args["posting_datetime"] = get_combine_datetime(args.posting_date, args.posting_time) ->>>>>>> d80ca523a4 (perf: new column posting datetime in SLE to optimize stock ledger related queries) if sle.get("voucher_type") == "Stock Reconciliation": # preserve previous_qty_after_transaction for qty reposting @@ -1416,23 +1398,6 @@ def get_valuation_rate( ) # Get valuation rate from last sle for the same item and warehouse -<<<<<<< HEAD - if not last_valuation_rate or last_valuation_rate[0][0] is None: - last_valuation_rate = frappe.db.sql( - """select valuation_rate - from `tabStock Ledger Entry` force index (item_warehouse) - where - item_code = %s - AND warehouse = %s - AND valuation_rate >= 0 - AND is_cancelled = 0 - AND NOT (voucher_no = %s AND voucher_type = %s) - order by posting_date desc, posting_time desc, name desc limit 1""", - (item_code, warehouse, voucher_no, voucher_type), - ) - - if last_valuation_rate: -======= if last_valuation_rate := frappe.db.sql( """select valuation_rate from `tabStock Ledger Entry` force index (item_warehouse) @@ -1445,7 +1410,6 @@ def get_valuation_rate( order by posting_datetime desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type), ): ->>>>>>> d80ca523a4 (perf: new column posting datetime in SLE to optimize stock ledger related queries) return flt(last_valuation_rate[0][0]) # If negative stock allowed, and item delivered without any incoming entry, @@ -1665,7 +1629,6 @@ def is_negative_with_precision(neg_sle, is_batch=False): return qty_deficit < 0 and abs(qty_deficit) > 0.0001 -<<<<<<< HEAD def get_future_sle_with_negative_qty(sle): SLE = frappe.qb.DocType("Stock Ledger Entry") query = ( @@ -1678,35 +1641,14 @@ def get_future_sle_with_negative_qty(sle): & (SLE.warehouse == sle.warehouse) & (SLE.voucher_no != sle.voucher_no) & ( - CombineDatetime(SLE.posting_date, SLE.posting_time) - >= CombineDatetime(sle.posting_date, sle.posting_time) + SLE.posting_datetime + >= get_combine_datetime(sle.posting_date, sle.posting_time) ) & (SLE.is_cancelled == 0) & (SLE.qty_after_transaction < 0) ) - .orderby(CombineDatetime(SLE.posting_date, SLE.posting_time)) + .orderby(SLE.posting_datetime) .limit(1) -======= -def get_future_sle_with_negative_qty(args): - return frappe.db.sql( - """ - select - qty_after_transaction, posting_date, posting_time, - voucher_type, voucher_no - from `tabStock Ledger Entry` - where - item_code = %(item_code)s - and warehouse = %(warehouse)s - and voucher_no != %(voucher_no)s - and posting_datetime >= %(posting_datetime)s - and is_cancelled = 0 - and qty_after_transaction < 0 - order by posting_datetime asc - limit 1 - """, - args, - as_dict=1, ->>>>>>> d80ca523a4 (perf: new column posting datetime in SLE to optimize stock ledger related queries) ) if sle.voucher_type == "Stock Reconciliation" and sle.batch_no: From a694a92df9c8133510e6c09021efbfc2cb98ff65 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 26 Feb 2024 13:19:32 +0530 Subject: [PATCH 42/94] chore: fix conflicts --- .../test_subcontracting_receipt.py | 253 ------------------ 1 file changed, 253 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index 0862970c1194..b05ed755c7f9 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -594,259 +594,6 @@ def test_supplied_items_cost_after_reposting(self): self.assertNotEqual(scr.supplied_items[0].rate, prev_cost) self.assertEqual(scr.supplied_items[0].rate, sr.items[0].valuation_rate) -<<<<<<< HEAD -======= - def test_subcontracting_receipt_for_batch_raw_materials_without_material_transfer(self): - set_backflush_based_on("BOM") - - fg_item = make_item(properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name - rm_item1 = make_item( - properties={ - "is_stock_item": 1, - "has_batch_no": 1, - "create_new_batch": 1, - "batch_number_series": "BNGS-.####", - } - ).name - - bom = make_bom(item=fg_item, raw_materials=[rm_item1]) - - rm_batch_no = None - for row in bom.items: - se = make_stock_entry( - item_code=row.item_code, - qty=1, - target="_Test Warehouse 1 - _TC", - rate=300, - ) - - se.reload() - rm_batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) - - service_items = [ - { - "warehouse": "_Test Warehouse - _TC", - "item_code": "Subcontracted Service Item 1", - "qty": 1, - "rate": 100, - "fg_item": fg_item, - "fg_item_qty": 1, - }, - ] - sco = get_subcontracting_order(service_items=service_items) - scr = make_subcontracting_receipt(sco.name) - scr.save() - scr.reload() - - bundle_doc = make_serial_batch_bundle( - { - "item_code": scr.supplied_items[0].rm_item_code, - "warehouse": "_Test Warehouse 1 - _TC", - "voucher_type": "Subcontracting Receipt", - "posting_date": today(), - "posting_time": nowtime(), - "qty": -1, - "batches": frappe._dict({rm_batch_no: 1}), - "type_of_transaction": "Outward", - "do_not_submit": True, - } - ) - - scr.supplied_items[0].serial_and_batch_bundle = bundle_doc.name - scr.submit() - scr.reload() - - batch_no = get_batch_from_bundle(scr.supplied_items[0].serial_and_batch_bundle) - self.assertEqual(batch_no, rm_batch_no) - self.assertEqual(scr.items[0].rm_cost_per_qty, 300) - self.assertEqual(scr.items[0].service_cost_per_qty, 100) - - def test_subcontracting_receipt_valuation_with_auto_created_serial_batch_bundle(self): - set_backflush_based_on("BOM") - - fg_item = make_item(properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name - rm_item1 = make_item( - properties={ - "is_stock_item": 1, - "has_batch_no": 1, - "create_new_batch": 1, - "batch_number_series": "BNGS-.####", - } - ).name - - rm_item2 = make_item( - properties={ - "is_stock_item": 1, - "has_batch_no": 1, - "has_serial_no": 1, - "create_new_batch": 1, - "batch_number_series": "BNGS-.####", - "serial_no_series": "BNSS-.####", - } - ).name - - rm_item3 = make_item( - properties={ - "is_stock_item": 1, - "has_serial_no": 1, - "serial_no_series": "BSSSS-.####", - } - ).name - - bom = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2, rm_item3]) - - rm_batch_no = None - for row in bom.items: - make_stock_entry( - item_code=row.item_code, - qty=1, - target="_Test Warehouse 1 - _TC", - rate=300, - ) - - make_stock_entry( - item_code=row.item_code, - qty=1, - target="_Test Warehouse 1 - _TC", - rate=400, - ) - - service_items = [ - { - "warehouse": "_Test Warehouse - _TC", - "item_code": "Subcontracted Service Item 1", - "qty": 1, - "rate": 100, - "fg_item": fg_item, - "fg_item_qty": 1, - }, - ] - sco = get_subcontracting_order(service_items=service_items) - - frappe.db.set_single_value( - "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 1 - ) - scr = make_subcontracting_receipt(sco.name) - scr.save() - scr.submit() - scr.reload() - - for row in scr.supplied_items: - self.assertEqual(row.rate, 300.00) - self.assertTrue(row.serial_and_batch_bundle) - auto_created_serial_batch = frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_no": scr.name, "voucher_detail_no": row.name}, - "auto_created_serial_and_batch_bundle", - ) - - self.assertTrue(auto_created_serial_batch) - - self.assertEqual(scr.items[0].rm_cost_per_qty, 900) - self.assertEqual(scr.items[0].service_cost_per_qty, 100) - frappe.db.set_single_value( - "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 0 - ) - - def test_subcontracting_receipt_valuation_for_fg_with_auto_created_serial_batch_bundle(self): - set_backflush_based_on("BOM") - - fg_item = make_item( - properties={ - "is_stock_item": 1, - "is_sub_contracted_item": 1, - "has_batch_no": 1, - "create_new_batch": 1, - "batch_number_series": "BSSNGS-.####", - } - ).name - - rm_item1 = make_item( - properties={ - "is_stock_item": 1, - "has_batch_no": 1, - "create_new_batch": 1, - "batch_number_series": "BNGS-.####", - } - ).name - - rm_item2 = make_item( - properties={ - "is_stock_item": 1, - "has_batch_no": 1, - "has_serial_no": 1, - "create_new_batch": 1, - "batch_number_series": "BNGS-.####", - "serial_no_series": "BNSS-.####", - } - ).name - - rm_item3 = make_item( - properties={ - "is_stock_item": 1, - "has_serial_no": 1, - "serial_no_series": "BSSSS-.####", - } - ).name - - bom = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2, rm_item3]) - - rm_batch_no = None - for row in bom.items: - make_stock_entry( - item_code=row.item_code, - qty=1, - target="_Test Warehouse 1 - _TC", - rate=300, - ) - - service_items = [ - { - "warehouse": "_Test Warehouse - _TC", - "item_code": "Subcontracted Service Item 1", - "qty": 1, - "rate": 100, - "fg_item": fg_item, - "fg_item_qty": 1, - }, - ] - sco = get_subcontracting_order(service_items=service_items) - - frappe.db.set_single_value( - "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 1 - ) - scr = make_subcontracting_receipt(sco.name) - scr.save() - scr.submit() - scr.reload() - - for row in scr.supplied_items: - self.assertEqual(row.rate, 300.00) - self.assertTrue(row.serial_and_batch_bundle) - auto_created_serial_batch = frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_no": scr.name, "voucher_detail_no": row.name}, - "auto_created_serial_and_batch_bundle", - ) - - self.assertTrue(auto_created_serial_batch) - - self.assertEqual(scr.items[0].rm_cost_per_qty, 900) - self.assertEqual(scr.items[0].service_cost_per_qty, 100) - self.assertEqual(scr.items[0].rate, 1000) - valuation_rate = frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_no": scr.name, "voucher_detail_no": scr.items[0].name}, - "valuation_rate", - ) - - self.assertEqual(flt(valuation_rate), flt(1000)) - - frappe.db.set_single_value( - "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 0 - ) - ->>>>>>> a73ba2c0d2 (chore: remove microsecond from posting_datetime) def test_subcontracting_receipt_raw_material_rate(self): from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom From 44d4096ba1ee152347626becbb9f279bc4e2e3e7 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 26 Feb 2024 13:20:56 +0530 Subject: [PATCH 43/94] chore: fix conflicts --- .../stock_reservation_entry.py | 1189 ----------------- 1 file changed, 1189 deletions(-) delete mode 100644 erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py deleted file mode 100644 index 26fe8e1787c2..000000000000 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ /dev/null @@ -1,1189 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from typing import Literal - -import frappe -from frappe import _ -from frappe.model.document import Document -from frappe.query_builder.functions import Sum -from frappe.utils import cint, flt, nowdate, nowtime - -from erpnext.stock.utils import get_or_make_bin, get_stock_balance - - -class StockReservationEntry(Document): - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - from erpnext.stock.doctype.serial_and_batch_entry.serial_and_batch_entry import ( - SerialandBatchEntry, - ) - - amended_from: DF.Link | None - available_qty: DF.Float - company: DF.Link | None - delivered_qty: DF.Float - from_voucher_detail_no: DF.Data | None - from_voucher_no: DF.DynamicLink | None - from_voucher_type: DF.Literal["", "Pick List", "Purchase Receipt"] - has_batch_no: DF.Check - has_serial_no: DF.Check - item_code: DF.Link | None - project: DF.Link | None - reservation_based_on: DF.Literal["Qty", "Serial and Batch"] - reserved_qty: DF.Float - sb_entries: DF.Table[SerialandBatchEntry] - status: DF.Literal[ - "Draft", "Partially Reserved", "Reserved", "Partially Delivered", "Delivered", "Cancelled" - ] - stock_uom: DF.Link | None - voucher_detail_no: DF.Data | None - voucher_no: DF.DynamicLink | None - voucher_qty: DF.Float - voucher_type: DF.Literal["", "Sales Order"] - warehouse: DF.Link | None - # end: auto-generated types - - def validate(self) -> None: - from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company - - self.validate_amended_doc() - self.validate_mandatory() - self.validate_group_warehouse() - validate_disabled_warehouse(self.warehouse) - validate_warehouse_company(self.warehouse, self.company) - self.validate_uom_is_integer() - - def before_submit(self) -> None: - self.set_reservation_based_on() - self.validate_reservation_based_on_qty() - self.auto_reserve_serial_and_batch() - self.validate_reservation_based_on_serial_and_batch() - - def on_submit(self) -> None: - self.update_reserved_qty_in_voucher() - self.update_reserved_qty_in_pick_list() - self.update_status() - self.update_reserved_stock_in_bin() - - def on_update_after_submit(self) -> None: - self.can_be_updated() - self.validate_uom_is_integer() - self.set_reservation_based_on() - self.validate_reservation_based_on_qty() - self.validate_reservation_based_on_serial_and_batch() - self.update_reserved_qty_in_voucher() - self.update_status() - self.update_reserved_stock_in_bin() - self.reload() - - def on_cancel(self) -> None: - self.update_reserved_qty_in_voucher() - self.update_reserved_qty_in_pick_list() - self.update_status() - self.update_reserved_stock_in_bin() - - def validate_amended_doc(self) -> None: - """Raises an exception if document is amended.""" - - if self.amended_from: - msg = _("Cannot amend {0} {1}, please create a new one instead.").format( - self.doctype, frappe.bold(self.amended_from) - ) - frappe.throw(msg) - - def validate_mandatory(self) -> None: - """Raises an exception if mandatory fields are not set.""" - - mandatory = [ - "item_code", - "warehouse", - "voucher_type", - "voucher_no", - "voucher_detail_no", - "available_qty", - "voucher_qty", - "stock_uom", - "reserved_qty", - "company", - ] - for d in mandatory: - if not self.get(d): - msg = _("{0} is required").format(self.meta.get_label(d)) - frappe.throw(msg) - - def validate_group_warehouse(self) -> None: - """Raises an exception if `Warehouse` is a Group Warehouse.""" - - if frappe.get_cached_value("Warehouse", self.warehouse, "is_group"): - msg = _("Stock cannot be reserved in group warehouse {0}.").format(frappe.bold(self.warehouse)) - frappe.throw(msg, title=_("Invalid Warehouse")) - - def validate_uom_is_integer(self) -> None: - """Validates `Reserved Qty` with Stock UOM.""" - - if cint(frappe.db.get_value("UOM", self.stock_uom, "must_be_whole_number", cache=True)): - if cint(self.reserved_qty) != flt(self.reserved_qty, self.precision("reserved_qty")): - msg = _( - "Reserved Qty ({0}) cannot be a fraction. To allow this, disable '{1}' in UOM {3}." - ).format( - flt(self.reserved_qty, self.precision("reserved_qty")), - frappe.bold(_("Must be Whole Number")), - frappe.bold(self.stock_uom), - ) - frappe.throw(msg) - - def set_reservation_based_on(self) -> None: - """Sets `Reservation Based On` based on `Has Serial No` and `Has Batch No`.""" - - if (self.reservation_based_on == "Serial and Batch") and ( - not self.has_serial_no and not self.has_batch_no - ): - self.db_set("reservation_based_on", "Qty") - - def validate_reservation_based_on_qty(self) -> None: - """Validates `Reserved Qty` when `Reservation Based On` is `Qty`.""" - - if self.reservation_based_on == "Qty": - self.validate_with_allowed_qty(self.reserved_qty) - - def auto_reserve_serial_and_batch(self, based_on: str = None) -> None: - """Auto pick Serial and Batch Nos to reserve when `Reservation Based On` is `Serial and Batch`.""" - - if ( - not self.from_voucher_type - and (self.get("_action") == "submit") - and (self.has_serial_no or self.has_batch_no) - and cint(frappe.db.get_single_value("Stock Settings", "auto_reserve_serial_and_batch")) - ): - from erpnext.stock.doctype.batch.batch import get_available_batches - from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos_for_outward - from erpnext.stock.serial_batch_bundle import get_serial_nos_batch - - self.reservation_based_on = "Serial and Batch" - self.sb_entries.clear() - kwargs = frappe._dict( - { - "item_code": self.item_code, - "warehouse": self.warehouse, - "qty": abs(self.reserved_qty) or 0, - "based_on": based_on - or frappe.db.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"), - } - ) - - serial_nos, batch_nos = [], [] - if self.has_serial_no: - serial_nos = get_serial_nos_for_outward(kwargs) - if self.has_batch_no: - batch_nos = get_available_batches(kwargs) - - if serial_nos: - serial_no_wise_batch = frappe._dict({}) - - if self.has_batch_no: - serial_no_wise_batch = get_serial_nos_batch(serial_nos) - - for serial_no in serial_nos: - self.append( - "sb_entries", - { - "serial_no": serial_no, - "qty": 1, - "batch_no": serial_no_wise_batch.get(serial_no), - "warehouse": self.warehouse, - }, - ) - elif batch_nos: - for batch_no, batch_qty in batch_nos.items(): - self.append( - "sb_entries", - { - "batch_no": batch_no, - "qty": batch_qty, - "warehouse": self.warehouse, - }, - ) - - def validate_reservation_based_on_serial_and_batch(self) -> None: - """Validates `Reserved Qty`, `Serial and Batch Nos` when `Reservation Based On` is `Serial and Batch`.""" - - if self.reservation_based_on == "Serial and Batch": - allow_partial_reservation = frappe.db.get_single_value( - "Stock Settings", "allow_partial_reservation" - ) - - available_serial_nos = [] - if self.has_serial_no: - available_serial_nos = get_available_serial_nos_to_reserve( - self.item_code, self.warehouse, self.has_batch_no, ignore_sre=self.name - ) - - if not available_serial_nos: - msg = _("Stock not available for Item {0} in Warehouse {1}.").format( - frappe.bold(self.item_code), frappe.bold(self.warehouse) - ) - frappe.throw(msg) - - qty_to_be_reserved = 0 - selected_batch_nos, selected_serial_nos = [], [] - for entry in self.sb_entries: - entry.warehouse = self.warehouse - - if self.has_serial_no: - entry.qty = 1 - - key = ( - (entry.serial_no, self.warehouse, entry.batch_no) - if self.has_batch_no - else (entry.serial_no, self.warehouse) - ) - if key not in available_serial_nos: - msg = _( - "Row #{0}: Serial No {1} for Item {2} is not available in {3} {4} or might be reserved in another {5}." - ).format( - entry.idx, - frappe.bold(entry.serial_no), - frappe.bold(self.item_code), - _("Batch {0} and Warehouse").format(frappe.bold(entry.batch_no)) - if self.has_batch_no - else _("Warehouse"), - frappe.bold(self.warehouse), - frappe.bold("Stock Reservation Entry"), - ) - - frappe.throw(msg) - - if entry.serial_no in selected_serial_nos: - msg = _("Row #{0}: Serial No {1} is already selected.").format( - entry.idx, frappe.bold(entry.serial_no) - ) - frappe.throw(msg) - else: - selected_serial_nos.append(entry.serial_no) - - elif self.has_batch_no: - if cint(frappe.db.get_value("Batch", entry.batch_no, "disabled")): - msg = _( - "Row #{0}: Stock cannot be reserved for Item {1} against a disabled Batch {2}." - ).format( - entry.idx, frappe.bold(self.item_code), frappe.bold(entry.batch_no) - ) - frappe.throw(msg) - - available_qty_to_reserve = get_available_qty_to_reserve( - self.item_code, self.warehouse, entry.batch_no, ignore_sre=self.name - ) - - if available_qty_to_reserve <= 0: - msg = _( - "Row #{0}: Stock not available to reserve for Item {1} against Batch {2} in Warehouse {3}." - ).format( - entry.idx, - frappe.bold(self.item_code), - frappe.bold(entry.batch_no), - frappe.bold(self.warehouse), - ) - frappe.throw(msg) - - if entry.qty > available_qty_to_reserve: - if allow_partial_reservation: - entry.qty = available_qty_to_reserve - if self.get("_action") == "update_after_submit": - entry.db_update() - else: - msg = _( - "Row #{0}: Qty should be less than or equal to Available Qty to Reserve (Actual Qty - Reserved Qty) {1} for Iem {2} against Batch {3} in Warehouse {4}." - ).format( - entry.idx, - frappe.bold(available_qty_to_reserve), - frappe.bold(self.item_code), - frappe.bold(entry.batch_no), - frappe.bold(self.warehouse), - ) - frappe.throw(msg) - - if entry.batch_no in selected_batch_nos: - msg = _("Row #{0}: Batch No {1} is already selected.").format( - entry.idx, frappe.bold(entry.batch_no) - ) - frappe.throw(msg) - else: - selected_batch_nos.append(entry.batch_no) - - qty_to_be_reserved += entry.qty - - if not qty_to_be_reserved: - msg = _("Please select Serial/Batch Nos to reserve or change Reservation Based On to Qty.") - frappe.throw(msg) - - # Should be called after validating Serial and Batch Nos. - self.validate_with_allowed_qty(qty_to_be_reserved) - self.db_set("reserved_qty", qty_to_be_reserved) - - def update_reserved_qty_in_voucher( - self, reserved_qty_field: str = "stock_reserved_qty", update_modified: bool = True - ) -> None: - """Updates total reserved qty in the voucher.""" - - item_doctype = "Sales Order Item" if self.voucher_type == "Sales Order" else None - - if item_doctype: - sre = frappe.qb.DocType("Stock Reservation Entry") - reserved_qty = ( - frappe.qb.from_(sre) - .select(Sum(sre.reserved_qty)) - .where( - (sre.docstatus == 1) - & (sre.voucher_type == self.voucher_type) - & (sre.voucher_no == self.voucher_no) - & (sre.voucher_detail_no == self.voucher_detail_no) - ) - ).run(as_list=True)[0][0] or 0 - - frappe.db.set_value( - item_doctype, - self.voucher_detail_no, - reserved_qty_field, - reserved_qty, - update_modified=update_modified, - ) - - def update_reserved_qty_in_pick_list( - self, reserved_qty_field: str = "stock_reserved_qty", update_modified: bool = True - ) -> None: - """Updates total reserved qty in the Pick List.""" - - if ( - self.from_voucher_type == "Pick List" and self.from_voucher_no and self.from_voucher_detail_no - ): - sre = frappe.qb.DocType("Stock Reservation Entry") - reserved_qty = ( - frappe.qb.from_(sre) - .select(Sum(sre.reserved_qty)) - .where( - (sre.docstatus == 1) - & (sre.from_voucher_type == "Pick List") - & (sre.from_voucher_no == self.from_voucher_no) - & (sre.from_voucher_detail_no == self.from_voucher_detail_no) - ) - ).run(as_list=True)[0][0] or 0 - - frappe.db.set_value( - "Pick List Item", - self.from_voucher_detail_no, - reserved_qty_field, - reserved_qty, - update_modified=update_modified, - ) - - def update_reserved_stock_in_bin(self) -> None: - """Updates `Reserved Stock` in Bin.""" - - bin_name = get_or_make_bin(self.item_code, self.warehouse) - bin_doc = frappe.get_cached_doc("Bin", bin_name) - bin_doc.update_reserved_stock() - - def update_status(self, status: str = None, update_modified: bool = True) -> None: - """Updates status based on Voucher Qty, Reserved Qty and Delivered Qty.""" - - if not status: - if self.docstatus == 2: - status = "Cancelled" - elif self.docstatus == 1: - if self.reserved_qty == self.delivered_qty: - status = "Delivered" - elif self.delivered_qty and self.delivered_qty < self.reserved_qty: - status = "Partially Delivered" - elif self.reserved_qty == self.voucher_qty: - status = "Reserved" - else: - status = "Partially Reserved" - else: - status = "Draft" - - frappe.db.set_value(self.doctype, self.name, "status", status, update_modified=update_modified) - - def can_be_updated(self) -> None: - """Raises an exception if `Stock Reservation Entry` is not allowed to be updated.""" - - if self.status in ("Partially Delivered", "Delivered"): - msg = _( - "{0} {1} cannot be updated. If you need to make changes, we recommend canceling the existing entry and creating a new one." - ).format(self.status, self.doctype) - frappe.throw(msg) - - if self.from_voucher_type == "Pick List": - msg = _( - "Stock Reservation Entry created against a Pick List cannot be updated. If you need to make changes, we recommend canceling the existing entry and creating a new one." - ) - frappe.throw(msg) - - if self.delivered_qty > 0: - msg = _("Stock Reservation Entry cannot be updated as it has been delivered.") - frappe.throw(msg) - - def validate_with_allowed_qty(self, qty_to_be_reserved: float) -> None: - """Validates `Reserved Qty` with `Max Reserved Qty`.""" - - self.db_set( - "available_qty", - get_available_qty_to_reserve(self.item_code, self.warehouse, ignore_sre=self.name), - ) - - total_reserved_qty = get_sre_reserved_qty_for_voucher_detail_no( - self.voucher_type, self.voucher_no, self.voucher_detail_no, ignore_sre=self.name - ) - - voucher_delivered_qty = 0 - if self.voucher_type == "Sales Order": - delivered_qty, conversion_factor = frappe.db.get_value( - "Sales Order Item", self.voucher_detail_no, ["delivered_qty", "conversion_factor"] - ) - voucher_delivered_qty = flt(delivered_qty) * flt(conversion_factor) - - allowed_qty = min( - self.available_qty, (self.voucher_qty - voucher_delivered_qty - total_reserved_qty) - ) - - if self.get("_action") != "submit" and self.voucher_type == "Sales Order" and allowed_qty <= 0: - msg = _("Item {0} is already reserved/delivered against Sales Order {1}.").format( - frappe.bold(self.item_code), frappe.bold(self.voucher_no) - ) - - if self.docstatus == 1: - self.cancel() - return frappe.msgprint(msg) - else: - frappe.throw(msg) - - if qty_to_be_reserved > allowed_qty: - actual_qty = get_stock_balance(self.item_code, self.warehouse) - msg = """ - Cannot reserve more than Allowed Qty {0} {1} for Item {2} against {3} {4}.

- The Allowed Qty is calculated as follows:
-
    -
  • Actual Qty [Available Qty at Warehouse] = {5}
  • -
  • Reserved Stock [Ignore current SRE] = {6}
  • -
  • Available Qty To Reserve [Actual Qty - Reserved Stock] = {7}
  • -
  • Voucher Qty [Voucher Item Qty] = {8}
  • -
  • Delivered Qty [Qty delivered against the Voucher Item] = {9}
  • -
  • Total Reserved Qty [Qty reserved against the Voucher Item] = {10}
  • -
  • Allowed Qty [Minimum of (Available Qty To Reserve, (Voucher Qty - Delivered Qty - Total Reserved Qty))] = {11}
  • -
- """.format( - frappe.bold(allowed_qty), - self.stock_uom, - frappe.bold(self.item_code), - self.voucher_type, - frappe.bold(self.voucher_no), - actual_qty, - actual_qty - self.available_qty, - self.available_qty, - self.voucher_qty, - voucher_delivered_qty, - total_reserved_qty, - allowed_qty, - ) - frappe.throw(msg) - - if qty_to_be_reserved <= self.delivered_qty: - msg = _("Reserved Qty should be greater than Delivered Qty.") - frappe.throw(msg) - - -def validate_stock_reservation_settings(voucher: object) -> None: - """Raises an exception if `Stock Reservation` is not enabled or `Voucher Type` is not allowed.""" - - if not frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"): - msg = _("Please enable {0} in the {1}.").format( - frappe.bold("Stock Reservation"), frappe.bold("Stock Settings") - ) - frappe.throw(msg) - - # Voucher types allowed for stock reservation - allowed_voucher_types = ["Sales Order"] - - if voucher.doctype not in allowed_voucher_types: - msg = _("Stock Reservation can only be created against {0}.").format( - ", ".join(allowed_voucher_types) - ) - frappe.throw(msg) - - -def get_available_qty_to_reserve( - item_code: str, warehouse: str, batch_no: str = None, ignore_sre=None -) -> float: - """Returns `Available Qty to Reserve (Actual Qty - Reserved Qty)` for Item, Warehouse and Batch combination.""" - - from erpnext.stock.doctype.batch.batch import get_batch_qty - - if batch_no: - return get_batch_qty( - item_code=item_code, warehouse=warehouse, batch_no=batch_no, ignore_voucher_nos=[ignore_sre] - ) - - available_qty = get_stock_balance(item_code, warehouse) - - if available_qty: - sre = frappe.qb.DocType("Stock Reservation Entry") - query = ( - frappe.qb.from_(sre) - .select(Sum(sre.reserved_qty - sre.delivered_qty)) - .where( - (sre.docstatus == 1) - & (sre.item_code == item_code) - & (sre.warehouse == warehouse) - & (sre.reserved_qty >= sre.delivered_qty) - & (sre.status.notin(["Delivered", "Cancelled"])) - ) - ) - - if ignore_sre: - query = query.where(sre.name != ignore_sre) - - reserved_qty = query.run()[0][0] or 0.0 - - if reserved_qty: - return available_qty - reserved_qty - - return available_qty - - -def get_available_serial_nos_to_reserve( - item_code: str, warehouse: str, has_batch_no: bool = False, ignore_sre=None -) -> list[tuple]: - """Returns Available Serial Nos to Reserve (Available Serial Nos - Reserved Serial Nos)` for Item, Warehouse and Batch combination.""" - - from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( - get_available_serial_nos, - ) - - available_serial_nos = get_available_serial_nos( - frappe._dict( - { - "item_code": item_code, - "warehouse": warehouse, - "has_batch_no": has_batch_no, - "ignore_voucher_nos": [ignore_sre], - } - ) - ) - - available_serial_nos_list = [] - if available_serial_nos: - available_serial_nos_list = [tuple(d.values()) for d in available_serial_nos] - - sre = frappe.qb.DocType("Stock Reservation Entry") - sb_entry = frappe.qb.DocType("Serial and Batch Entry") - query = ( - frappe.qb.from_(sre) - .left_join(sb_entry) - .on(sre.name == sb_entry.parent) - .select(sb_entry.serial_no, sre.warehouse) - .where( - (sre.docstatus == 1) - & (sre.item_code == item_code) - & (sre.warehouse == warehouse) - & (sre.reserved_qty >= sre.delivered_qty) - & (sre.status.notin(["Delivered", "Cancelled"])) - & (sre.reservation_based_on == "Serial and Batch") - ) - ) - - if has_batch_no: - query = query.select(sb_entry.batch_no) - - if ignore_sre: - query = query.where(sre.name != ignore_sre) - - reserved_serial_nos = query.run() - - if reserved_serial_nos: - return list(set(available_serial_nos_list) - set(reserved_serial_nos)) - - return available_serial_nos_list - - -def get_sre_reserved_qty_for_item_and_warehouse(item_code: str, warehouse: str = None) -> float: - """Returns current `Reserved Qty` for Item and Warehouse combination.""" - - sre = frappe.qb.DocType("Stock Reservation Entry") - query = ( - frappe.qb.from_(sre) - .select(Sum(sre.reserved_qty - sre.delivered_qty).as_("reserved_qty")) - .where( - (sre.docstatus == 1) - & (sre.item_code == item_code) - & (sre.status.notin(["Delivered", "Cancelled"])) - ) - .groupby(sre.item_code, sre.warehouse) - ) - - if warehouse: - query = query.where(sre.warehouse == warehouse) - - reserved_qty = query.run(as_list=True) - - return flt(reserved_qty[0][0]) if reserved_qty else 0.0 - - -def get_sre_reserved_qty_for_items_and_warehouses( - item_code_list: list, warehouse_list: list = None -) -> dict: - """Returns a dict like {("item_code", "warehouse"): "reserved_qty", ... }.""" - - if not item_code_list: - return {} - - sre = frappe.qb.DocType("Stock Reservation Entry") - query = ( - frappe.qb.from_(sre) - .select( - sre.item_code, - sre.warehouse, - Sum(sre.reserved_qty - sre.delivered_qty).as_("reserved_qty"), - ) - .where( - (sre.docstatus == 1) - & sre.item_code.isin(item_code_list) - & (sre.status.notin(["Delivered", "Cancelled"])) - ) - .groupby(sre.item_code, sre.warehouse) - ) - - if warehouse_list: - query = query.where(sre.warehouse.isin(warehouse_list)) - - data = query.run(as_dict=True) - - return {(d["item_code"], d["warehouse"]): d["reserved_qty"] for d in data} if data else {} - - -def get_sre_reserved_qty_details_for_voucher(voucher_type: str, voucher_no: str) -> dict: - """Returns a dict like {"voucher_detail_no": "reserved_qty", ... }.""" - - sre = frappe.qb.DocType("Stock Reservation Entry") - data = ( - frappe.qb.from_(sre) - .select( - sre.voucher_detail_no, - (Sum(sre.reserved_qty) - Sum(sre.delivered_qty)).as_("reserved_qty"), - ) - .where( - (sre.docstatus == 1) - & (sre.voucher_type == voucher_type) - & (sre.voucher_no == voucher_no) - & (sre.status.notin(["Delivered", "Cancelled"])) - ) - .groupby(sre.voucher_detail_no) - ).run(as_list=True) - - return frappe._dict(data) - - -def get_sre_reserved_warehouses_for_voucher( - voucher_type: str, voucher_no: str, voucher_detail_no: str = None -) -> list: - """Returns a list of warehouses where the stock is reserved for the provided voucher.""" - - sre = frappe.qb.DocType("Stock Reservation Entry") - query = ( - frappe.qb.from_(sre) - .select(sre.warehouse) - .distinct() - .where( - (sre.docstatus == 1) - & (sre.voucher_type == voucher_type) - & (sre.voucher_no == voucher_no) - & (sre.status.notin(["Delivered", "Cancelled"])) - ) - .orderby(sre.creation) - ) - - if voucher_detail_no: - query = query.where(sre.voucher_detail_no == voucher_detail_no) - - warehouses = query.run(as_list=True) - - return [d[0] for d in warehouses] if warehouses else [] - - -def get_sre_reserved_qty_for_voucher_detail_no( - voucher_type: str, voucher_no: str, voucher_detail_no: str, ignore_sre=None -) -> float: - """Returns `Reserved Qty` against the Voucher.""" - - sre = frappe.qb.DocType("Stock Reservation Entry") - query = ( - frappe.qb.from_(sre) - .select( - (Sum(sre.reserved_qty) - Sum(sre.delivered_qty)), - ) - .where( - (sre.docstatus == 1) - & (sre.voucher_type == voucher_type) - & (sre.voucher_no == voucher_no) - & (sre.voucher_detail_no == voucher_detail_no) - & (sre.status.notin(["Delivered", "Cancelled"])) - ) - ) - - if ignore_sre: - query = query.where(sre.name != ignore_sre) - - reserved_qty = query.run(as_list=True) - - return flt(reserved_qty[0][0]) - - -def get_sre_reserved_serial_nos_details( - item_code: str, warehouse: str, serial_nos: list = None -) -> dict: - """Returns a dict of `Serial No` reserved in Stock Reservation Entry. The dict is like {serial_no: sre_name, ...}""" - - sre = frappe.qb.DocType("Stock Reservation Entry") - sb_entry = frappe.qb.DocType("Serial and Batch Entry") - query = ( - frappe.qb.from_(sre) - .inner_join(sb_entry) - .on(sre.name == sb_entry.parent) - .select(sb_entry.serial_no, sre.name) - .where( - (sre.docstatus == 1) - & (sre.item_code == item_code) - & (sre.warehouse == warehouse) - & (sre.reserved_qty > sre.delivered_qty) - & (sre.status.notin(["Delivered", "Cancelled"])) - & (sre.reservation_based_on == "Serial and Batch") - ) - .orderby(sb_entry.creation) - ) - - if serial_nos: - query = query.where(sb_entry.serial_no.isin(serial_nos)) - - return frappe._dict(query.run()) - - -def get_sre_reserved_batch_nos_details( - item_code: str, warehouse: str, batch_nos: list = None -) -> dict: - """Returns a dict of `Batch Qty` reserved in Stock Reservation Entry. The dict is like {batch_no: qty, ...}""" - - sre = frappe.qb.DocType("Stock Reservation Entry") - sb_entry = frappe.qb.DocType("Serial and Batch Entry") - query = ( - frappe.qb.from_(sre) - .inner_join(sb_entry) - .on(sre.name == sb_entry.parent) - .select( - sb_entry.batch_no, - Sum(sb_entry.qty - sb_entry.delivered_qty), - ) - .where( - (sre.docstatus == 1) - & (sre.item_code == item_code) - & (sre.warehouse == warehouse) - & ((sre.reserved_qty - sre.delivered_qty) > 0) - & (sre.status.notin(["Delivered", "Cancelled"])) - & (sre.reservation_based_on == "Serial and Batch") - ) - .groupby(sb_entry.batch_no) - .orderby(sb_entry.creation) - ) - - if batch_nos: - query = query.where(sb_entry.batch_no.isin(batch_nos)) - - return frappe._dict(query.run()) - - -def get_sre_details_for_voucher(voucher_type: str, voucher_no: str) -> list[dict]: - """Returns a list of SREs for the provided voucher.""" - - sre = frappe.qb.DocType("Stock Reservation Entry") - return ( - frappe.qb.from_(sre) - .select( - sre.name, - sre.item_code, - sre.warehouse, - sre.voucher_type, - sre.voucher_no, - sre.voucher_detail_no, - (sre.reserved_qty - sre.delivered_qty).as_("reserved_qty"), - sre.has_serial_no, - sre.has_batch_no, - sre.reservation_based_on, - ) - .where( - (sre.docstatus == 1) - & (sre.voucher_type == voucher_type) - & (sre.voucher_no == voucher_no) - & (sre.reserved_qty > sre.delivered_qty) - & (sre.status.notin(["Delivered", "Cancelled"])) - ) - .orderby(sre.creation) - ).run(as_dict=True) - - -def get_serial_batch_entries_for_voucher(sre_name: str) -> list[dict]: - """Returns a list of `Serial and Batch Entries` for the provided voucher.""" - - sre = frappe.qb.DocType("Stock Reservation Entry") - sb_entry = frappe.qb.DocType("Serial and Batch Entry") - - return ( - frappe.qb.from_(sre) - .inner_join(sb_entry) - .on(sre.name == sb_entry.parent) - .select( - sb_entry.serial_no, - sb_entry.batch_no, - (sb_entry.qty - sb_entry.delivered_qty).as_("qty"), - ) - .where( - (sre.docstatus == 1) & (sre.name == sre_name) & (sre.status.notin(["Delivered", "Cancelled"])) - ) - .where(sb_entry.qty > sb_entry.delivered_qty) - .orderby(sb_entry.creation) - ).run(as_dict=True) - - -def get_ssb_bundle_for_voucher(sre: dict) -> object: - """Returns a new `Serial and Batch Bundle` against the provided SRE.""" - - sb_entries = get_serial_batch_entries_for_voucher(sre["name"]) - - if sb_entries: - bundle = frappe.new_doc("Serial and Batch Bundle") - bundle.type_of_transaction = "Outward" - bundle.voucher_type = "Delivery Note" - bundle.posting_date = nowdate() - bundle.posting_time = nowtime() - - for field in ("item_code", "warehouse", "has_serial_no", "has_batch_no"): - setattr(bundle, field, sre[field]) - - for sb_entry in sb_entries: - bundle.append("entries", sb_entry) - - bundle.save() - - return bundle.name - - -def has_reserved_stock(voucher_type: str, voucher_no: str, voucher_detail_no: str = None) -> bool: - """Returns True if there is any Stock Reservation Entry for the given voucher.""" - - if get_stock_reservation_entries_for_voucher( - voucher_type, voucher_no, voucher_detail_no, fields=["name"], ignore_status=True - ): - return True - - return False - - -def create_stock_reservation_entries_for_so_items( - sales_order: object, - items_details: list[dict] = None, - from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None, - notify=True, -) -> None: - """Creates Stock Reservation Entries for Sales Order Items.""" - - from erpnext.selling.doctype.sales_order.sales_order import get_unreserved_qty - - if not from_voucher_type and ( - sales_order.get("_action") == "submit" - and sales_order.set_warehouse - and cint(frappe.get_cached_value("Warehouse", sales_order.set_warehouse, "is_group")) - ): - return frappe.msgprint( - _("Stock cannot be reserved in the group warehouse {0}.").format( - frappe.bold(sales_order.set_warehouse) - ) - ) - - validate_stock_reservation_settings(sales_order) - - allow_partial_reservation = frappe.db.get_single_value( - "Stock Settings", "allow_partial_reservation" - ) - - items = [] - if items_details: - for item in items_details: - so_item = frappe.get_doc("Sales Order Item", item.get("sales_order_item")) - so_item.warehouse = item.get("warehouse") - so_item.qty_to_reserve = ( - flt(item.get("qty_to_reserve")) - if from_voucher_type in ["Pick List", "Purchase Receipt"] - else ( - flt(item.get("qty_to_reserve")) - * (flt(item.get("conversion_factor")) or flt(so_item.conversion_factor) or 1) - ) - ) - so_item.from_voucher_no = item.get("from_voucher_no") - so_item.from_voucher_detail_no = item.get("from_voucher_detail_no") - so_item.serial_and_batch_bundle = item.get("serial_and_batch_bundle") - - items.append(so_item) - - sre_count = 0 - reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", sales_order.name) - - for item in items if items_details else sales_order.get("items"): - # Skip if `Reserved Stock` is not checked for the item. - if not item.get("reserve_stock"): - continue - - # Stock should be reserved from the Pick List if has Picked Qty. - if from_voucher_type != "Pick List" and flt(item.picked_qty) > 0: - frappe.throw( - _("Row #{0}: Item {1} has been picked, please reserve stock from the Pick List.").format( - item.idx, frappe.bold(item.item_code) - ) - ) - - is_stock_item, has_serial_no, has_batch_no = frappe.get_cached_value( - "Item", item.item_code, ["is_stock_item", "has_serial_no", "has_batch_no"] - ) - - # Skip if Non-Stock Item. - if not is_stock_item: - if not from_voucher_type: - frappe.msgprint( - _("Row #{0}: Stock cannot be reserved for a non-stock Item {1}").format( - item.idx, frappe.bold(item.item_code) - ), - title=_("Stock Reservation"), - indicator="yellow", - ) - - item.db_set("reserve_stock", 0) - continue - - # Skip if Group Warehouse. - if frappe.get_cached_value("Warehouse", item.warehouse, "is_group"): - frappe.msgprint( - _("Row #{0}: Stock cannot be reserved in group warehouse {1}.").format( - item.idx, frappe.bold(item.warehouse) - ), - title=_("Stock Reservation"), - indicator="yellow", - ) - continue - - unreserved_qty = get_unreserved_qty(item, reserved_qty_details) - - # Stock is already reserved for the item, notify the user and skip the item. - if unreserved_qty <= 0: - if not from_voucher_type: - frappe.msgprint( - _("Row #{0}: Stock is already reserved for the Item {1}.").format( - item.idx, frappe.bold(item.item_code) - ), - title=_("Stock Reservation"), - indicator="yellow", - ) - - continue - - available_qty_to_reserve = get_available_qty_to_reserve(item.item_code, item.warehouse) - - # No stock available to reserve, notify the user and skip the item. - if available_qty_to_reserve <= 0: - frappe.msgprint( - _("Row #{0}: Stock not available to reserve for the Item {1} in Warehouse {2}.").format( - item.idx, frappe.bold(item.item_code), frappe.bold(item.warehouse) - ), - title=_("Stock Reservation"), - indicator="orange", - ) - continue - - # The quantity which can be reserved. - qty_to_be_reserved = min(unreserved_qty, available_qty_to_reserve) - - if hasattr(item, "qty_to_reserve"): - if item.qty_to_reserve <= 0: - frappe.msgprint( - _("Row #{0}: Quantity to reserve for the Item {1} should be greater than 0.").format( - item.idx, frappe.bold(item.item_code) - ), - title=_("Stock Reservation"), - indicator="orange", - ) - continue - else: - qty_to_be_reserved = min(qty_to_be_reserved, item.qty_to_reserve) - - # Partial Reservation - if qty_to_be_reserved < unreserved_qty: - if not from_voucher_type and ( - not item.get("qty_to_reserve") or qty_to_be_reserved < flt(item.get("qty_to_reserve")) - ): - msg = _("Row #{0}: Only {1} available to reserve for the Item {2}").format( - item.idx, - frappe.bold(str(qty_to_be_reserved / item.conversion_factor) + " " + item.uom), - frappe.bold(item.item_code), - ) - frappe.msgprint(msg, title=_("Stock Reservation"), indicator="orange") - - # Skip the item if `Partial Reservation` is disabled in the Stock Settings. - if not allow_partial_reservation: - if qty_to_be_reserved == flt(item.get("qty_to_reserve")): - msg = _("Enable Allow Partial Reservation in the Stock Settings to reserve partial stock.") - frappe.msgprint(msg, title=_("Partial Stock Reservation"), indicator="yellow") - - continue - - sre = frappe.new_doc("Stock Reservation Entry") - - sre.item_code = item.item_code - sre.warehouse = item.warehouse - sre.has_serial_no = has_serial_no - sre.has_batch_no = has_batch_no - sre.voucher_type = sales_order.doctype - sre.voucher_no = sales_order.name - sre.voucher_detail_no = item.name - sre.available_qty = available_qty_to_reserve - sre.voucher_qty = item.stock_qty - sre.reserved_qty = qty_to_be_reserved - sre.company = sales_order.company - sre.stock_uom = item.stock_uom - sre.project = sales_order.project - - if from_voucher_type: - sre.from_voucher_type = from_voucher_type - sre.from_voucher_no = item.from_voucher_no - sre.from_voucher_detail_no = item.from_voucher_detail_no - - if item.get("serial_and_batch_bundle"): - sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle) - sre.reservation_based_on = "Serial and Batch" - - index, picked_qty = 0, 0 - while index < len(sbb.entries) and picked_qty < qty_to_be_reserved: - entry = sbb.entries[index] - qty = 1 if has_serial_no else min(abs(entry.qty), qty_to_be_reserved - picked_qty) - - sre.append( - "sb_entries", - { - "serial_no": entry.serial_no, - "batch_no": entry.batch_no, - "qty": qty, - "warehouse": entry.warehouse, - }, - ) - - index += 1 - picked_qty += qty - - sre.save() - sre.submit() - - sre_count += 1 - - if sre_count and notify: - frappe.msgprint(_("Stock Reservation Entries Created"), alert=True, indicator="green") - - -def cancel_stock_reservation_entries( - voucher_type: str = None, - voucher_no: str = None, - voucher_detail_no: str = None, - from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None, - from_voucher_no: str = None, - from_voucher_detail_no: str = None, - sre_list: list = None, - notify: bool = True, -) -> None: - """Cancel Stock Reservation Entries.""" - - if not sre_list: - sre_list = {} - - if voucher_type and voucher_no: - sre_list = get_stock_reservation_entries_for_voucher( - voucher_type, voucher_no, voucher_detail_no, fields=["name"] - ) - elif from_voucher_type and from_voucher_no: - sre = frappe.qb.DocType("Stock Reservation Entry") - query = ( - frappe.qb.from_(sre) - .select(sre.name) - .where( - (sre.docstatus == 1) - & (sre.from_voucher_type == from_voucher_type) - & (sre.from_voucher_no == from_voucher_no) - & (sre.status.notin(["Delivered", "Cancelled"])) - ) - .orderby(sre.creation) - ) - - if from_voucher_detail_no: - query = query.where(sre.from_voucher_detail_no == from_voucher_detail_no) - - sre_list = query.run(as_dict=True) - - sre_list = [d.name for d in sre_list] - - if sre_list: - for sre in sre_list: - frappe.get_doc("Stock Reservation Entry", sre).cancel() - - if notify: - msg = _("Stock Reservation Entries Cancelled") - frappe.msgprint(msg, alert=True, indicator="red") - - -@frappe.whitelist() -def get_stock_reservation_entries_for_voucher( - voucher_type: str, - voucher_no: str, - voucher_detail_no: str = None, - fields: list[str] = None, - ignore_status: bool = False, -) -> list[dict]: - """Returns list of Stock Reservation Entries against a Voucher.""" - - if not fields or not isinstance(fields, list): - fields = [ - "name", - "item_code", - "warehouse", - "voucher_detail_no", - "reserved_qty", - "delivered_qty", - "stock_uom", - ] - - sre = frappe.qb.DocType("Stock Reservation Entry") - query = ( - frappe.qb.from_(sre) - .where( - (sre.docstatus == 1) & (sre.voucher_type == voucher_type) & (sre.voucher_no == voucher_no) - ) - .orderby(sre.creation) - ) - - for field in fields: - query = query.select(sre[field]) - - if voucher_detail_no: - query = query.where(sre.voucher_detail_no == voucher_detail_no) - - if ignore_status: - query = query.where(sre.status.notin(["Delivered", "Cancelled"])) - - return query.run(as_dict=True) From c36a5d1ba3987c858d759239ce557bc9f15890ca Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 26 Feb 2024 13:35:11 +0530 Subject: [PATCH 44/94] chore: fix backport issues --- erpnext/accounts/utils.py | 40 ++ .../test_serial_and_batch_bundle.py | 590 ------------------ erpnext/stock/stock_ledger.py | 9 +- 3 files changed, 43 insertions(+), 596 deletions(-) delete mode 100644 erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index b6520d3c82d7..6ca4aa2ada6b 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -931,6 +931,46 @@ def get_currency_precision(): return precision +def get_stock_rbnb_difference(posting_date, company): + stock_items = frappe.db.sql_list( + """select distinct item_code + from `tabStock Ledger Entry` where company=%s""", + company, + ) + + pr_valuation_amount = frappe.db.sql( + """ + select sum(pr_item.valuation_rate * pr_item.qty * pr_item.conversion_factor) + from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr + where pr.name = pr_item.parent and pr.docstatus=1 and pr.company=%s + and pr.posting_date <= %s and pr_item.item_code in (%s)""" + % ("%s", "%s", ", ".join(["%s"] * len(stock_items))), + tuple([company, posting_date] + stock_items), + )[0][0] + + pi_valuation_amount = frappe.db.sql( + """ + select sum(pi_item.valuation_rate * pi_item.qty * pi_item.conversion_factor) + from `tabPurchase Invoice Item` pi_item, `tabPurchase Invoice` pi + where pi.name = pi_item.parent and pi.docstatus=1 and pi.company=%s + and pi.posting_date <= %s and pi_item.item_code in (%s)""" + % ("%s", "%s", ", ".join(["%s"] * len(stock_items))), + tuple([company, posting_date] + stock_items), + )[0][0] + + # Balance should be + stock_rbnb = flt(pr_valuation_amount, 2) - flt(pi_valuation_amount, 2) + + # Balance as per system + stock_rbnb_account = "Stock Received But Not Billed - " + frappe.get_cached_value( + "Company", company, "abbr" + ) + sys_bal = get_balance_on(stock_rbnb_account, posting_date, in_account_currency=False) + + # Amount should be credited + return flt(stock_rbnb) + flt(sys_bal) + + def get_held_invoices(party_type, party): """ Returns a list of names Purchase Invoices for the given party that are on hold diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py deleted file mode 100644 index b932c1371d6e..000000000000 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ /dev/null @@ -1,590 +0,0 @@ -# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import json - -import frappe -from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import flt, nowtime, today - -from erpnext.stock.doctype.item.test_item import make_item -from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( - add_serial_batch_ledgers, - make_batch_nos, - make_serial_nos, -) -from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry - - -class TestSerialandBatchBundle(FrappeTestCase): - def test_inward_outward_serial_valuation(self): - from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note - from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt - - serial_item_code = "New Serial No Valuation 1" - make_item( - serial_item_code, - { - "has_serial_no": 1, - "serial_no_series": "TEST-SER-VAL-.#####", - "is_stock_item": 1, - }, - ) - - pr = make_purchase_receipt( - item_code=serial_item_code, warehouse="_Test Warehouse - _TC", qty=1, rate=500 - ) - - serial_no1 = get_serial_nos_from_bundle(pr.items[0].serial_and_batch_bundle)[0] - - pr = make_purchase_receipt( - item_code=serial_item_code, warehouse="_Test Warehouse - _TC", qty=1, rate=300 - ) - - serial_no2 = get_serial_nos_from_bundle(pr.items[0].serial_and_batch_bundle)[0] - - dn = create_delivery_note( - item_code=serial_item_code, - warehouse="_Test Warehouse - _TC", - qty=1, - rate=1500, - serial_no=[serial_no2], - ) - - stock_value_difference = frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_no": dn.name, "is_cancelled": 0, "voucher_type": "Delivery Note"}, - "stock_value_difference", - ) - - self.assertEqual(flt(stock_value_difference, 2), -300) - - dn = create_delivery_note( - item_code=serial_item_code, - warehouse="_Test Warehouse - _TC", - qty=1, - rate=1500, - serial_no=[serial_no1], - ) - - stock_value_difference = frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_no": dn.name, "is_cancelled": 0, "voucher_type": "Delivery Note"}, - "stock_value_difference", - ) - - self.assertEqual(flt(stock_value_difference, 2), -500) - - def test_inward_outward_batch_valuation(self): - from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note - from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt - - batch_item_code = "New Batch No Valuation 1" - make_item( - batch_item_code, - { - "has_batch_no": 1, - "create_new_batch": 1, - "batch_number_series": "TEST-BATTCCH-VAL-.#####", - "is_stock_item": 1, - }, - ) - - pr = make_purchase_receipt( - item_code=batch_item_code, warehouse="_Test Warehouse - _TC", qty=10, rate=500 - ) - - batch_no1 = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle) - - pr = make_purchase_receipt( - item_code=batch_item_code, warehouse="_Test Warehouse - _TC", qty=10, rate=300 - ) - - batch_no2 = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle) - - dn = create_delivery_note( - item_code=batch_item_code, - warehouse="_Test Warehouse - _TC", - qty=10, - rate=1500, - batch_no=batch_no2, - ) - - stock_value_difference = frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_no": dn.name, "is_cancelled": 0, "voucher_type": "Delivery Note"}, - "stock_value_difference", - ) - - self.assertEqual(flt(stock_value_difference, 2), -3000) - - dn = create_delivery_note( - item_code=batch_item_code, - warehouse="_Test Warehouse - _TC", - qty=10, - rate=1500, - batch_no=batch_no1, - ) - - stock_value_difference = frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_no": dn.name, "is_cancelled": 0, "voucher_type": "Delivery Note"}, - "stock_value_difference", - ) - - self.assertEqual(flt(stock_value_difference, 2), -5000) - - def test_old_batch_valuation(self): - frappe.flags.ignore_serial_batch_bundle_validation = True - frappe.flags.use_serial_and_batch_fields = True - batch_item_code = "Old Batch Item Valuation 1" - make_item( - batch_item_code, - { - "has_batch_no": 1, - "is_stock_item": 1, - }, - ) - - batch_id = "Old Batch 1" - if not frappe.db.exists("Batch", batch_id): - batch_doc = frappe.get_doc( - { - "doctype": "Batch", - "batch_id": batch_id, - "item": batch_item_code, - "use_batchwise_valuation": 0, - } - ).insert(ignore_permissions=True) - - self.assertTrue(batch_doc.use_batchwise_valuation) - batch_doc.db_set("use_batchwise_valuation", 0) - - stock_queue = [] - qty_after_transaction = 0 - balance_value = 0 - for qty, valuation in {10: 100, 20: 200}.items(): - stock_queue.append([qty, valuation]) - qty_after_transaction += qty - balance_value += qty_after_transaction * valuation - - doc = frappe.get_doc( - { - "doctype": "Stock Ledger Entry", - "posting_date": today(), - "posting_time": nowtime(), - "batch_no": batch_id, - "incoming_rate": valuation, - "qty_after_transaction": qty_after_transaction, - "stock_value_difference": valuation * qty, - "balance_value": balance_value, - "valuation_rate": balance_value / qty_after_transaction, - "actual_qty": qty, - "item_code": batch_item_code, - "warehouse": "_Test Warehouse - _TC", - "stock_queue": json.dumps(stock_queue), - } - ) - - doc.flags.ignore_permissions = True - doc.flags.ignore_mandatory = True - doc.flags.ignore_links = True - doc.flags.ignore_validate = True - doc.submit() - doc.reload() - - bundle_doc = make_serial_batch_bundle( - { - "item_code": batch_item_code, - "warehouse": "_Test Warehouse - _TC", - "voucher_type": "Stock Entry", - "posting_date": today(), - "posting_time": nowtime(), - "qty": -10, - "batches": frappe._dict({batch_id: 10}), - "type_of_transaction": "Outward", - "do_not_submit": True, - } - ) - - bundle_doc.reload() - for row in bundle_doc.entries: - self.assertEqual(flt(row.stock_value_difference, 2), -1666.67) - - bundle_doc.flags.ignore_permissions = True - bundle_doc.flags.ignore_mandatory = True - bundle_doc.flags.ignore_links = True - bundle_doc.flags.ignore_validate = True - bundle_doc.submit() - - bundle_doc = make_serial_batch_bundle( - { - "item_code": batch_item_code, - "warehouse": "_Test Warehouse - _TC", - "voucher_type": "Stock Entry", - "posting_date": today(), - "posting_time": nowtime(), - "qty": -20, - "batches": frappe._dict({batch_id: 20}), - "type_of_transaction": "Outward", - "do_not_submit": True, - } - ) - - bundle_doc.reload() - for row in bundle_doc.entries: - self.assertEqual(flt(row.stock_value_difference, 2), -3333.33) - - bundle_doc.flags.ignore_permissions = True - bundle_doc.flags.ignore_mandatory = True - bundle_doc.flags.ignore_links = True - bundle_doc.flags.ignore_validate = True - bundle_doc.submit() - - frappe.flags.ignore_serial_batch_bundle_validation = False - frappe.flags.use_serial_and_batch_fields = False - - def test_old_serial_no_valuation(self): - from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt - - serial_no_item_code = "Old Serial No Item Valuation 1" - make_item( - serial_no_item_code, - { - "has_serial_no": 1, - "serial_no_series": "TEST-SER-VALL-.#####", - "is_stock_item": 1, - }, - ) - - make_purchase_receipt( - item_code=serial_no_item_code, warehouse="_Test Warehouse - _TC", qty=1, rate=500 - ) - - frappe.flags.ignore_serial_batch_bundle_validation = True - frappe.flags.use_serial_and_batch_fields = True - - serial_no_id = "Old Serial No 1" - if not frappe.db.exists("Serial No", serial_no_id): - sn_doc = frappe.get_doc( - { - "doctype": "Serial No", - "serial_no": serial_no_id, - "item_code": serial_no_item_code, - "company": "_Test Company", - } - ).insert(ignore_permissions=True) - - sn_doc.db_set( - { - "warehouse": "_Test Warehouse - _TC", - "purchase_rate": 100, - } - ) - - doc = frappe.get_doc( - { - "doctype": "Stock Ledger Entry", - "posting_date": today(), - "posting_time": nowtime(), - "serial_no": serial_no_id, - "incoming_rate": 100, - "qty_after_transaction": 1, - "stock_value_difference": 100, - "balance_value": 100, - "valuation_rate": 100, - "actual_qty": 1, - "item_code": serial_no_item_code, - "warehouse": "_Test Warehouse - _TC", - "company": "_Test Company", - } - ) - - doc.flags.ignore_permissions = True - doc.flags.ignore_mandatory = True - doc.flags.ignore_links = True - doc.flags.ignore_validate = True - doc.submit() - - bundle_doc = make_serial_batch_bundle( - { - "item_code": serial_no_item_code, - "warehouse": "_Test Warehouse - _TC", - "voucher_type": "Stock Entry", - "posting_date": today(), - "posting_time": nowtime(), - "qty": -1, - "serial_nos": [serial_no_id], - "type_of_transaction": "Outward", - "do_not_submit": True, - } - ) - - bundle_doc.reload() - for row in bundle_doc.entries: - self.assertEqual(flt(row.stock_value_difference, 2), -100.00) - - frappe.flags.ignore_serial_batch_bundle_validation = False - frappe.flags.use_serial_and_batch_fields = False - - def test_batch_not_belong_to_serial_no(self): - from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt - - serial_and_batch_code = "New Serial No Valuation 1" - make_item( - serial_and_batch_code, - { - "has_serial_no": 1, - "serial_no_series": "TEST-SER-VALL-.#####", - "is_stock_item": 1, - "has_batch_no": 1, - "create_new_batch": 1, - "batch_number_series": "TEST-SNBAT-VAL-.#####", - }, - ) - - pr = make_purchase_receipt( - item_code=serial_and_batch_code, warehouse="_Test Warehouse - _TC", qty=1, rate=500 - ) - - serial_no = get_serial_nos_from_bundle(pr.items[0].serial_and_batch_bundle)[0] - - pr = make_purchase_receipt( - item_code=serial_and_batch_code, warehouse="_Test Warehouse - _TC", qty=1, rate=300 - ) - - batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle) - - doc = frappe.get_doc( - { - "doctype": "Serial and Batch Bundle", - "item_code": serial_and_batch_code, - "warehouse": "_Test Warehouse - _TC", - "voucher_type": "Stock Entry", - "posting_date": today(), - "posting_time": nowtime(), - "qty": -1, - "type_of_transaction": "Outward", - } - ) - - doc.append( - "entries", - { - "batch_no": batch_no, - "serial_no": serial_no, - "qty": -1, - }, - ) - - # Batch does not belong to serial no - self.assertRaises(frappe.exceptions.ValidationError, doc.save) - - def test_auto_delete_draft_serial_and_batch_bundle(self): - serial_and_batch_code = "New Serial No Auto Delete 1" - make_item( - serial_and_batch_code, - { - "has_serial_no": 1, - "serial_no_series": "TEST-SER-VALL-.#####", - "is_stock_item": 1, - }, - ) - - ste = make_stock_entry( - item_code=serial_and_batch_code, - target="_Test Warehouse - _TC", - qty=1, - rate=500, - do_not_submit=True, - ) - - serial_no = "SN-TEST-AUTO-DEL" - if not frappe.db.exists("Serial No", serial_no): - frappe.get_doc( - { - "doctype": "Serial No", - "serial_no": serial_no, - "item_code": serial_and_batch_code, - "company": "_Test Company", - } - ).insert(ignore_permissions=True) - - bundle_doc = make_serial_batch_bundle( - { - "item_code": serial_and_batch_code, - "warehouse": "_Test Warehouse - _TC", - "voucher_type": "Stock Entry", - "posting_date": ste.posting_date, - "posting_time": ste.posting_time, - "qty": 1, - "serial_nos": [serial_no], - "type_of_transaction": "Inward", - "do_not_submit": True, - } - ) - - bundle_doc.reload() - ste.items[0].serial_and_batch_bundle = bundle_doc.name - ste.save() - ste.reload() - - ste.delete() - self.assertFalse(frappe.db.exists("Serial and Batch Bundle", bundle_doc.name)) - - def test_serial_and_batch_bundle_company(self): - 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, - warehouse="_Test Warehouse - _TC", - qty=3, - rate=500, - do_not_submit=True, - ) - - entries = [] - for serial_no in ["TT-SER-VAL-00001", "TT-SER-VAL-00002", "TT-SER-VAL-00003"]: - entries.append(frappe._dict({"serial_no": serial_no, "qty": 1})) - - if not frappe.db.exists("Serial No", serial_no): - frappe.get_doc( - { - "doctype": "Serial No", - "serial_no": serial_no, - "item_code": item, - } - ).insert(ignore_permissions=True) - - item_row = pr.items[0] - item_row.type_of_transaction = "Inward" - item_row.is_rejected = 0 - 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 test_batch_duplicate_entry(self): - item_code = make_item(properties={"has_batch_no": 1}).name - - batch_id = "TEST-BATTCCH-VAL-00001" - batch_nos = [{"batch_no": batch_id, "qty": 1}] - - make_batch_nos(item_code, batch_nos) - self.assertTrue(frappe.db.exists("Batch", batch_id)) - - batch_id = "TEST-BATTCCH-VAL-00001" - batch_nos = [{"batch_no": batch_id, "qty": 1}] - - # Shouldn't throw duplicate entry error - make_batch_nos(item_code, batch_nos) - self.assertTrue(frappe.db.exists("Batch", batch_id)) - - def test_serial_no_duplicate_entry(self): - item_code = make_item(properties={"has_serial_no": 1}).name - - serial_no_id = "TEST-SNID-VAL-00001" - serial_nos = [{"serial_no": serial_no_id, "qty": 1}] - - make_serial_nos(item_code, serial_nos) - self.assertTrue(frappe.db.exists("Serial No", serial_no_id)) - - serial_no_id = "TEST-SNID-VAL-00001" - serial_nos = [{"batch_no": serial_no_id, "qty": 1}] - - # Shouldn't throw duplicate entry error - make_serial_nos(item_code, serial_nos) - self.assertTrue(frappe.db.exists("Serial No", serial_no_id)) - - @change_settings("Stock Settings", {"auto_create_serial_and_batch_bundle_for_outward": 1}) - def test_duplicate_serial_and_batch_bundle(self): - from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt - - item_code = make_item(properties={"is_stock_item": 1, "has_serial_no": 1}).name - - serial_no = f"{item_code}-001" - serial_nos = [{"serial_no": serial_no, "qty": 1}] - make_serial_nos(item_code, serial_nos) - - pr1 = make_purchase_receipt(item=item_code, qty=1, rate=500, serial_no=[serial_no]) - pr2 = make_purchase_receipt(item=item_code, qty=1, rate=500, do_not_save=True) - - pr1.reload() - pr2.items[0].serial_and_batch_bundle = pr1.items[0].serial_and_batch_bundle - - self.assertRaises(frappe.exceptions.ValidationError, pr2.save) - - -def get_batch_from_bundle(bundle): - from erpnext.stock.serial_batch_bundle import get_batch_nos - - batches = get_batch_nos(bundle) - - return list(batches.keys())[0] - - -def get_serial_nos_from_bundle(bundle): - from erpnext.stock.serial_batch_bundle import get_serial_nos - - serial_nos = get_serial_nos(bundle) - return sorted(serial_nos) if serial_nos else [] - - -def make_serial_batch_bundle(kwargs): - from erpnext.stock.serial_batch_bundle import SerialBatchCreation - - if isinstance(kwargs, dict): - kwargs = frappe._dict(kwargs) - - type_of_transaction = "Inward" if kwargs.qty > 0 else "Outward" - if kwargs.get("type_of_transaction"): - type_of_transaction = kwargs.get("type_of_transaction") - - sb = SerialBatchCreation( - { - "item_code": kwargs.item_code, - "warehouse": kwargs.warehouse, - "voucher_type": kwargs.voucher_type, - "voucher_no": kwargs.voucher_no, - "posting_date": kwargs.posting_date, - "posting_time": kwargs.posting_time, - "qty": kwargs.qty, - "avg_rate": kwargs.rate, - "batches": kwargs.batches, - "serial_nos": kwargs.serial_nos, - "type_of_transaction": type_of_transaction, - "company": kwargs.company or "_Test Company", - "do_not_submit": kwargs.do_not_submit, - } - ) - - if not kwargs.get("do_not_save"): - return sb.make_serial_and_batch_bundle() - - return sb diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index ce1ffa52edee..c09a30d22c95 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -7,8 +7,8 @@ import frappe from frappe import _ from frappe.model.meta import get_field_precision -from frappe.query_builder.functions import CombineDatetime, Sum -from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate, nowtime, parse_json +from frappe.query_builder.functions import Sum +from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate import erpnext from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty @@ -1640,10 +1640,7 @@ def get_future_sle_with_negative_qty(sle): (SLE.item_code == sle.item_code) & (SLE.warehouse == sle.warehouse) & (SLE.voucher_no != sle.voucher_no) - & ( - SLE.posting_datetime - >= get_combine_datetime(sle.posting_date, sle.posting_time) - ) + & (SLE.posting_datetime >= get_combine_datetime(sle.posting_date, sle.posting_time)) & (SLE.is_cancelled == 0) & (SLE.qty_after_transaction < 0) ) From 77349a0af671351939633dbe7c0db63cd17bd8ca Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 26 Feb 2024 14:42:18 +0530 Subject: [PATCH 45/94] chore: fix test case --- .../purchase_receipt/test_purchase_receipt.py | 2 +- erpnext/stock/stock_ledger.py | 27 ++++++++++--------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 0d244da1fdc2..d4f85b1aa7ef 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -3,7 +3,7 @@ import frappe from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import add_days, cint, cstr, flt, today, nowtime +from frappe.utils import add_days, cint, cstr, flt, nowtime, today from pypika import functions as fn import erpnext diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index c09a30d22c95..a642a1ff7ed8 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1398,18 +1398,21 @@ def get_valuation_rate( ) # Get valuation rate from last sle for the same item and warehouse - if last_valuation_rate := frappe.db.sql( - """select valuation_rate - from `tabStock Ledger Entry` force index (item_warehouse) - where - item_code = %s - AND warehouse = %s - AND valuation_rate >= 0 - AND is_cancelled = 0 - AND NOT (voucher_no = %s AND voucher_type = %s) - order by posting_datetime desc, name desc limit 1""", - (item_code, warehouse, voucher_no, voucher_type), - ): + if not last_valuation_rate or last_valuation_rate[0][0] is None: + last_valuation_rate = frappe.db.sql( + """select valuation_rate + from `tabStock Ledger Entry` force index (item_warehouse) + where + item_code = %s + AND warehouse = %s + AND valuation_rate >= 0 + AND is_cancelled = 0 + AND NOT (voucher_no = %s AND voucher_type = %s) + order by posting_datetime desc, name desc limit 1""", + (item_code, warehouse, voucher_no, voucher_type), + ) + + if last_valuation_rate: return flt(last_valuation_rate[0][0]) # If negative stock allowed, and item delivered without any incoming entry, From 957f55f6b1b15c6a17e62741c102ceb397148f5b Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 20 Mar 2024 20:12:36 +0530 Subject: [PATCH 46/94] chore: resolve conflicts --- erpnext/buying/doctype/purchase_order/purchase_order.json | 4 ---- .../buying/doctype/supplier_quotation/supplier_quotation.json | 4 ---- erpnext/selling/doctype/quotation/quotation.json | 4 ---- erpnext/stock/doctype/delivery_note/delivery_note.json | 4 ---- 4 files changed, 16 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index a2bb42bfe7d7..0230e499f4fd 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -1273,11 +1273,7 @@ "idx": 105, "is_submittable": 1, "links": [], -<<<<<<< HEAD - "modified": "2023-10-01 20:58:07.851037", -======= "modified": "2024-03-20 16:03:31.611808", ->>>>>>> 1c63983873 (fix: use Text Editor for rendering tax breakup table) "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json index 7716a15ccbab..f13ceb04a50e 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json @@ -927,11 +927,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], -<<<<<<< HEAD - "modified": "2023-11-20 11:15:30.083077", -======= "modified": "2024-03-20 16:03:59.069145", ->>>>>>> 1c63983873 (fix: use Text Editor for rendering tax breakup table) "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation", diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index f3c5439d9879..40fa986951e2 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -1072,11 +1072,7 @@ "idx": 82, "is_submittable": 1, "links": [], -<<<<<<< HEAD - "modified": "2023-04-14 16:50:44.550098", -======= "modified": "2024-03-20 16:04:21.567847", ->>>>>>> 1c63983873 (fix: use Text Editor for rendering tax breakup table) "modified_by": "Administrator", "module": "Selling", "name": "Quotation", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index ec22e5501453..60e69599dae7 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -1401,11 +1401,7 @@ "idx": 146, "is_submittable": 1, "links": [], -<<<<<<< HEAD - "modified": "2023-12-18 17:19:39.368239", -======= "modified": "2024-03-20 16:05:02.854990", ->>>>>>> 1c63983873 (fix: use Text Editor for rendering tax breakup table) "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", From cf3b0ee41ecadcf4eea4179d110c5ccfa9030e5d Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 20 Mar 2024 20:15:20 +0530 Subject: [PATCH 47/94] chore: resolve conflicts --- .../doctype/pos_invoice/pos_invoice.py | 166 ---------------- .../purchase_invoice/purchase_invoice.py | 175 ---------------- .../doctype/sales_invoice/sales_invoice.py | 188 ------------------ .../doctype/purchase_order/purchase_order.py | 135 ------------- .../supplier_quotation/supplier_quotation.py | 92 --------- erpnext/public/scss/erpnext.scss | 55 ----- .../selling/doctype/quotation/quotation.py | 106 ---------- .../doctype/sales_order/sales_order.py | 138 ------------- .../doctype/delivery_note/delivery_note.py | 125 ------------ .../purchase_receipt/purchase_receipt.py | 115 ----------- 10 files changed, 1295 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 18eee1d9562f..383b9dab24a2 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -26,172 +26,6 @@ class POSInvoice(SalesInvoice): -<<<<<<< HEAD -======= - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule - from erpnext.accounts.doctype.pos_invoice_item.pos_invoice_item import POSInvoiceItem - from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail - from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import ( - SalesInvoiceAdvance, - ) - from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import ( - SalesInvoicePayment, - ) - from erpnext.accounts.doctype.sales_invoice_timesheet.sales_invoice_timesheet import ( - SalesInvoiceTimesheet, - ) - from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import ( - SalesTaxesandCharges, - ) - from erpnext.selling.doctype.sales_team.sales_team import SalesTeam - from erpnext.stock.doctype.packed_item.packed_item import PackedItem - - account_for_change_amount: DF.Link | None - additional_discount_percentage: DF.Float - address_display: DF.SmallText | None - advances: DF.Table[SalesInvoiceAdvance] - against_income_account: DF.SmallText | None - allocate_advances_automatically: DF.Check - amended_from: DF.Link | None - amount_eligible_for_commission: DF.Currency - apply_discount_on: DF.Literal["", "Grand Total", "Net Total"] - auto_repeat: DF.Link | None - base_change_amount: DF.Currency - base_discount_amount: DF.Currency - base_grand_total: DF.Currency - base_in_words: DF.Data | None - base_net_total: DF.Currency - base_paid_amount: DF.Currency - base_rounded_total: DF.Currency - base_rounding_adjustment: DF.Currency - base_total: DF.Currency - base_total_taxes_and_charges: DF.Currency - base_write_off_amount: DF.Currency - campaign: DF.Link | None - cash_bank_account: DF.Link | None - change_amount: DF.Currency - commission_rate: DF.Float - company: DF.Link - company_address: DF.Link | None - company_address_display: DF.SmallText | None - consolidated_invoice: DF.Link | None - contact_display: DF.SmallText | None - contact_email: DF.Data | None - contact_mobile: DF.Data | None - contact_person: DF.Link | None - conversion_rate: DF.Float - cost_center: DF.Link | None - coupon_code: DF.Link | None - currency: DF.Link - customer: DF.Link | None - customer_address: DF.Link | None - customer_group: DF.Link | None - customer_name: DF.Data | None - debit_to: DF.Link - discount_amount: DF.Currency - due_date: DF.Date | None - from_date: DF.Date | None - grand_total: DF.Currency - group_same_items: DF.Check - ignore_pricing_rule: DF.Check - in_words: DF.Data | None - inter_company_invoice_reference: DF.Link | None - is_discounted: DF.Check - is_opening: DF.Literal["No", "Yes"] - is_pos: DF.Check - is_return: DF.Check - items: DF.Table[POSInvoiceItem] - language: DF.Data | None - letter_head: DF.Link | None - loyalty_amount: DF.Currency - loyalty_points: DF.Int - loyalty_program: DF.Link | None - loyalty_redemption_account: DF.Link | None - loyalty_redemption_cost_center: DF.Link | None - naming_series: DF.Literal["ACC-PSINV-.YYYY.-"] - net_total: DF.Currency - other_charges_calculation: DF.TextEditor | None - outstanding_amount: DF.Currency - packed_items: DF.Table[PackedItem] - paid_amount: DF.Currency - party_account_currency: DF.Link | None - payment_schedule: DF.Table[PaymentSchedule] - payment_terms_template: DF.Link | None - payments: DF.Table[SalesInvoicePayment] - plc_conversion_rate: DF.Float - po_date: DF.Date | None - po_no: DF.Data | None - pos_profile: DF.Link | None - posting_date: DF.Date - posting_time: DF.Time | None - price_list_currency: DF.Link - pricing_rules: DF.Table[PricingRuleDetail] - project: DF.Link | None - redeem_loyalty_points: DF.Check - remarks: DF.SmallText | None - return_against: DF.Link | None - rounded_total: DF.Currency - rounding_adjustment: DF.Currency - sales_partner: DF.Link | None - sales_team: DF.Table[SalesTeam] - scan_barcode: DF.Data | None - select_print_heading: DF.Link | None - selling_price_list: DF.Link - set_posting_time: DF.Check - set_warehouse: DF.Link | None - shipping_address: DF.SmallText | None - shipping_address_name: DF.Link | None - shipping_rule: DF.Link | None - source: DF.Link | None - status: DF.Literal[ - "", - "Draft", - "Return", - "Credit Note Issued", - "Consolidated", - "Submitted", - "Paid", - "Unpaid", - "Unpaid and Discounted", - "Overdue and Discounted", - "Overdue", - "Cancelled", - ] - tax_category: DF.Link | None - tax_id: DF.Data | None - taxes: DF.Table[SalesTaxesandCharges] - taxes_and_charges: DF.Link | None - tc_name: DF.Link | None - terms: DF.TextEditor | None - territory: DF.Link | None - timesheets: DF.Table[SalesInvoiceTimesheet] - title: DF.Data | None - to_date: DF.Date | None - total: DF.Currency - total_advance: DF.Currency - total_billing_amount: DF.Currency - total_commission: DF.Currency - total_net_weight: DF.Float - total_qty: DF.Float - total_taxes_and_charges: DF.Currency - update_billed_amount_in_delivery_note: DF.Check - update_billed_amount_in_sales_order: DF.Check - update_stock: DF.Check - write_off_account: DF.Link | None - write_off_amount: DF.Currency - write_off_cost_center: DF.Link | None - write_off_outstanding_amount_automatically: DF.Check - # end: auto-generated types - ->>>>>>> 1c63983873 (fix: use Text Editor for rendering tax breakup table) def __init__(self, *args, **kwargs): super(POSInvoice, self).__init__(*args, **kwargs) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index beff6d95ca2e..f54787de717e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -54,181 +54,6 @@ class WarehouseMissingError(frappe.ValidationError): class PurchaseInvoice(BuyingController): -<<<<<<< HEAD -======= - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - from erpnext.accounts.doctype.advance_tax.advance_tax import AdvanceTax - from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule - from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail - from erpnext.accounts.doctype.purchase_invoice_advance.purchase_invoice_advance import ( - PurchaseInvoiceAdvance, - ) - from erpnext.accounts.doctype.purchase_invoice_item.purchase_invoice_item import ( - PurchaseInvoiceItem, - ) - from erpnext.accounts.doctype.purchase_taxes_and_charges.purchase_taxes_and_charges import ( - PurchaseTaxesandCharges, - ) - from erpnext.accounts.doctype.tax_withheld_vouchers.tax_withheld_vouchers import ( - TaxWithheldVouchers, - ) - from erpnext.buying.doctype.purchase_receipt_item_supplied.purchase_receipt_item_supplied import ( - PurchaseReceiptItemSupplied, - ) - - additional_discount_percentage: DF.Float - address_display: DF.SmallText | None - advance_tax: DF.Table[AdvanceTax] - advances: DF.Table[PurchaseInvoiceAdvance] - against_expense_account: DF.SmallText | None - allocate_advances_automatically: DF.Check - amended_from: DF.Link | None - apply_discount_on: DF.Literal["", "Grand Total", "Net Total"] - apply_tds: DF.Check - auto_repeat: DF.Link | None - base_discount_amount: DF.Currency - base_grand_total: DF.Currency - base_in_words: DF.Data | None - base_net_total: DF.Currency - base_paid_amount: DF.Currency - base_rounded_total: DF.Currency - base_rounding_adjustment: DF.Currency - base_tax_withholding_net_total: DF.Currency - base_taxes_and_charges_added: DF.Currency - base_taxes_and_charges_deducted: DF.Currency - base_total: DF.Currency - base_total_taxes_and_charges: DF.Currency - base_write_off_amount: DF.Currency - bill_date: DF.Date | None - bill_no: DF.Data | None - billing_address: DF.Link | None - billing_address_display: DF.SmallText | None - buying_price_list: DF.Link | None - cash_bank_account: DF.Link | None - clearance_date: DF.Date | None - company: DF.Link | None - contact_display: DF.SmallText | None - contact_email: DF.SmallText | None - contact_mobile: DF.SmallText | None - contact_person: DF.Link | None - conversion_rate: DF.Float - cost_center: DF.Link | None - credit_to: DF.Link - currency: DF.Link | None - disable_rounded_total: DF.Check - discount_amount: DF.Currency - due_date: DF.Date | None - from_date: DF.Date | None - grand_total: DF.Currency - group_same_items: DF.Check - hold_comment: DF.SmallText | None - ignore_default_payment_terms_template: DF.Check - ignore_pricing_rule: DF.Check - in_words: DF.Data | None - incoterm: DF.Link | None - inter_company_invoice_reference: DF.Link | None - is_internal_supplier: DF.Check - is_old_subcontracting_flow: DF.Check - is_opening: DF.Literal["No", "Yes"] - is_paid: DF.Check - is_return: DF.Check - is_subcontracted: DF.Check - items: DF.Table[PurchaseInvoiceItem] - language: DF.Data | None - letter_head: DF.Link | None - mode_of_payment: DF.Link | None - named_place: DF.Data | None - naming_series: DF.Literal["ACC-PINV-.YYYY.-", "ACC-PINV-RET-.YYYY.-"] - net_total: DF.Currency - on_hold: DF.Check - only_include_allocated_payments: DF.Check - other_charges_calculation: DF.TextEditor | None - outstanding_amount: DF.Currency - paid_amount: DF.Currency - party_account_currency: DF.Link | None - payment_schedule: DF.Table[PaymentSchedule] - payment_terms_template: DF.Link | None - per_received: DF.Percent - plc_conversion_rate: DF.Float - posting_date: DF.Date - posting_time: DF.Time | None - price_list_currency: DF.Link | None - pricing_rules: DF.Table[PricingRuleDetail] - project: DF.Link | None - rejected_warehouse: DF.Link | None - release_date: DF.Date | None - remarks: DF.SmallText | None - repost_required: DF.Check - represents_company: DF.Link | None - return_against: DF.Link | None - rounded_total: DF.Currency - rounding_adjustment: DF.Currency - scan_barcode: DF.Data | None - select_print_heading: DF.Link | None - set_from_warehouse: DF.Link | None - set_posting_time: DF.Check - set_warehouse: DF.Link | None - shipping_address: DF.Link | None - shipping_address_display: DF.SmallText | None - shipping_rule: DF.Link | None - status: DF.Literal[ - "", - "Draft", - "Return", - "Debit Note Issued", - "Submitted", - "Paid", - "Partly Paid", - "Unpaid", - "Overdue", - "Cancelled", - "Internal Transfer", - ] - subscription: DF.Link | None - supplied_items: DF.Table[PurchaseReceiptItemSupplied] - supplier: DF.Link - supplier_address: DF.Link | None - supplier_group: DF.Link | None - supplier_name: DF.Data | None - supplier_warehouse: DF.Link | None - tax_category: DF.Link | None - tax_id: DF.ReadOnly | None - tax_withheld_vouchers: DF.Table[TaxWithheldVouchers] - tax_withholding_category: DF.Link | None - tax_withholding_net_total: DF.Currency - taxes: DF.Table[PurchaseTaxesandCharges] - taxes_and_charges: DF.Link | None - taxes_and_charges_added: DF.Currency - taxes_and_charges_deducted: DF.Currency - tc_name: DF.Link | None - terms: DF.TextEditor | None - title: DF.Data | None - to_date: DF.Date | None - total: DF.Currency - total_advance: DF.Currency - total_net_weight: DF.Float - total_qty: DF.Float - total_taxes_and_charges: DF.Currency - unrealized_profit_loss_account: DF.Link | None - update_billed_amount_in_purchase_order: DF.Check - update_billed_amount_in_purchase_receipt: DF.Check - update_outstanding_for_self: DF.Check - update_stock: DF.Check - use_company_roundoff_cost_center: DF.Check - use_transaction_date_exchange_rate: DF.Check - write_off_account: DF.Link | None - write_off_amount: DF.Currency - write_off_cost_center: DF.Link | None - # end: auto-generated types - ->>>>>>> 1c63983873 (fix: use Text Editor for rendering tax breakup table) def __init__(self, *args, **kwargs): super(PurchaseInvoice, self).__init__(*args, **kwargs) self.status_updater = [ diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 02f5259f4aa3..2a86d0daf45e 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -49,194 +49,6 @@ class SalesInvoice(SellingController): -<<<<<<< HEAD -======= - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule - from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail - from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import ( - SalesInvoiceAdvance, - ) - from erpnext.accounts.doctype.sales_invoice_item.sales_invoice_item import SalesInvoiceItem - from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import ( - SalesInvoicePayment, - ) - from erpnext.accounts.doctype.sales_invoice_timesheet.sales_invoice_timesheet import ( - SalesInvoiceTimesheet, - ) - from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import ( - SalesTaxesandCharges, - ) - from erpnext.selling.doctype.sales_team.sales_team import SalesTeam - from erpnext.stock.doctype.packed_item.packed_item import PackedItem - - account_for_change_amount: DF.Link | None - additional_discount_account: DF.Link | None - additional_discount_percentage: DF.Float - address_display: DF.SmallText | None - advances: DF.Table[SalesInvoiceAdvance] - against_income_account: DF.SmallText | None - allocate_advances_automatically: DF.Check - amended_from: DF.Link | None - amount_eligible_for_commission: DF.Currency - apply_discount_on: DF.Literal["", "Grand Total", "Net Total"] - auto_repeat: DF.Link | None - base_change_amount: DF.Currency - base_discount_amount: DF.Currency - base_grand_total: DF.Currency - base_in_words: DF.SmallText | None - base_net_total: DF.Currency - base_paid_amount: DF.Currency - base_rounded_total: DF.Currency - base_rounding_adjustment: DF.Currency - base_total: DF.Currency - base_total_taxes_and_charges: DF.Currency - base_write_off_amount: DF.Currency - campaign: DF.Link | None - cash_bank_account: DF.Link | None - change_amount: DF.Currency - commission_rate: DF.Float - company: DF.Link - company_address: DF.Link | None - company_address_display: DF.SmallText | None - company_tax_id: DF.Data | None - contact_display: DF.SmallText | None - contact_email: DF.Data | None - contact_mobile: DF.SmallText | None - contact_person: DF.Link | None - conversion_rate: DF.Float - cost_center: DF.Link | None - currency: DF.Link - customer: DF.Link | None - customer_address: DF.Link | None - customer_group: DF.Link | None - customer_name: DF.SmallText | None - debit_to: DF.Link - disable_rounded_total: DF.Check - discount_amount: DF.Currency - dispatch_address: DF.SmallText | None - dispatch_address_name: DF.Link | None - dont_create_loyalty_points: DF.Check - due_date: DF.Date | None - from_date: DF.Date | None - grand_total: DF.Currency - group_same_items: DF.Check - ignore_default_payment_terms_template: DF.Check - ignore_pricing_rule: DF.Check - in_words: DF.SmallText | None - incoterm: DF.Link | None - inter_company_invoice_reference: DF.Link | None - is_cash_or_non_trade_discount: DF.Check - is_consolidated: DF.Check - is_debit_note: DF.Check - is_discounted: DF.Check - is_internal_customer: DF.Check - is_opening: DF.Literal["No", "Yes"] - is_pos: DF.Check - is_return: DF.Check - items: DF.Table[SalesInvoiceItem] - language: DF.Data | None - letter_head: DF.Link | None - loyalty_amount: DF.Currency - loyalty_points: DF.Int - loyalty_program: DF.Link | None - loyalty_redemption_account: DF.Link | None - loyalty_redemption_cost_center: DF.Link | None - named_place: DF.Data | None - naming_series: DF.Literal["ACC-SINV-.YYYY.-", "ACC-SINV-RET-.YYYY.-"] - net_total: DF.Currency - only_include_allocated_payments: DF.Check - other_charges_calculation: DF.TextEditor | None - outstanding_amount: DF.Currency - packed_items: DF.Table[PackedItem] - paid_amount: DF.Currency - party_account_currency: DF.Link | None - payment_schedule: DF.Table[PaymentSchedule] - payment_terms_template: DF.Link | None - payments: DF.Table[SalesInvoicePayment] - plc_conversion_rate: DF.Float - po_date: DF.Date | None - po_no: DF.Data | None - pos_profile: DF.Link | None - posting_date: DF.Date - posting_time: DF.Time | None - price_list_currency: DF.Link - pricing_rules: DF.Table[PricingRuleDetail] - project: DF.Link | None - redeem_loyalty_points: DF.Check - remarks: DF.SmallText | None - repost_required: DF.Check - represents_company: DF.Link | None - return_against: DF.Link | None - rounded_total: DF.Currency - rounding_adjustment: DF.Currency - sales_partner: DF.Link | None - sales_team: DF.Table[SalesTeam] - scan_barcode: DF.Data | None - select_print_heading: DF.Link | None - selling_price_list: DF.Link - set_posting_time: DF.Check - set_target_warehouse: DF.Link | None - set_warehouse: DF.Link | None - shipping_address: DF.SmallText | None - shipping_address_name: DF.Link | None - shipping_rule: DF.Link | None - source: DF.Link | None - status: DF.Literal[ - "", - "Draft", - "Return", - "Credit Note Issued", - "Submitted", - "Paid", - "Partly Paid", - "Unpaid", - "Unpaid and Discounted", - "Partly Paid and Discounted", - "Overdue and Discounted", - "Overdue", - "Cancelled", - "Internal Transfer", - ] - subscription: DF.Link | None - tax_category: DF.Link | None - tax_id: DF.Data | None - taxes: DF.Table[SalesTaxesandCharges] - taxes_and_charges: DF.Link | None - tc_name: DF.Link | None - terms: DF.TextEditor | None - territory: DF.Link | None - timesheets: DF.Table[SalesInvoiceTimesheet] - title: DF.Data | None - to_date: DF.Date | None - total: DF.Currency - total_advance: DF.Currency - total_billing_amount: DF.Currency - total_billing_hours: DF.Float - total_commission: DF.Currency - total_net_weight: DF.Float - total_qty: DF.Float - total_taxes_and_charges: DF.Currency - unrealized_profit_loss_account: DF.Link | None - update_billed_amount_in_delivery_note: DF.Check - update_billed_amount_in_sales_order: DF.Check - update_outstanding_for_self: DF.Check - update_stock: DF.Check - use_company_roundoff_cost_center: DF.Check - write_off_account: DF.Link | None - write_off_amount: DF.Currency - write_off_cost_center: DF.Link | None - write_off_outstanding_amount_automatically: DF.Check - # end: auto-generated types - ->>>>>>> 1c63983873 (fix: use Text Editor for rendering tax breakup table) def __init__(self, *args, **kwargs): super(SalesInvoice, self).__init__(*args, **kwargs) self.status_updater = [ diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index cca058f806aa..9d4846056eaf 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -33,141 +33,6 @@ class PurchaseOrder(BuyingController): -<<<<<<< HEAD -======= - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule - from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail - from erpnext.accounts.doctype.purchase_taxes_and_charges.purchase_taxes_and_charges import ( - PurchaseTaxesandCharges, - ) - from erpnext.buying.doctype.purchase_order_item.purchase_order_item import PurchaseOrderItem - from erpnext.buying.doctype.purchase_order_item_supplied.purchase_order_item_supplied import ( - PurchaseOrderItemSupplied, - ) - - additional_discount_percentage: DF.Float - address_display: DF.SmallText | None - advance_paid: DF.Currency - advance_payment_status: DF.Literal["Not Initiated", "Initiated", "Partially Paid", "Fully Paid"] - amended_from: DF.Link | None - apply_discount_on: DF.Literal["", "Grand Total", "Net Total"] - apply_tds: DF.Check - auto_repeat: DF.Link | None - base_discount_amount: DF.Currency - base_grand_total: DF.Currency - base_in_words: DF.Data | None - base_net_total: DF.Currency - base_rounded_total: DF.Currency - base_rounding_adjustment: DF.Currency - base_tax_withholding_net_total: DF.Currency - base_taxes_and_charges_added: DF.Currency - base_taxes_and_charges_deducted: DF.Currency - base_total: DF.Currency - base_total_taxes_and_charges: DF.Currency - billing_address: DF.Link | None - billing_address_display: DF.SmallText | None - buying_price_list: DF.Link | None - company: DF.Link - contact_display: DF.SmallText | None - contact_email: DF.SmallText | None - contact_mobile: DF.SmallText | None - contact_person: DF.Link | None - conversion_rate: DF.Float - cost_center: DF.Link | None - currency: DF.Link - customer: DF.Link | None - customer_contact_display: DF.SmallText | None - customer_contact_email: DF.Code | None - customer_contact_mobile: DF.SmallText | None - customer_contact_person: DF.Link | None - customer_name: DF.Data | None - disable_rounded_total: DF.Check - discount_amount: DF.Currency - from_date: DF.Date | None - grand_total: DF.Currency - group_same_items: DF.Check - ignore_pricing_rule: DF.Check - in_words: DF.Data | None - incoterm: DF.Link | None - inter_company_order_reference: DF.Link | None - is_internal_supplier: DF.Check - is_old_subcontracting_flow: DF.Check - is_subcontracted: DF.Check - items: DF.Table[PurchaseOrderItem] - language: DF.Data | None - letter_head: DF.Link | None - named_place: DF.Data | None - naming_series: DF.Literal["PUR-ORD-.YYYY.-"] - net_total: DF.Currency - order_confirmation_date: DF.Date | None - order_confirmation_no: DF.Data | None - other_charges_calculation: DF.TextEditor | None - party_account_currency: DF.Link | None - payment_schedule: DF.Table[PaymentSchedule] - payment_terms_template: DF.Link | None - per_billed: DF.Percent - per_received: DF.Percent - plc_conversion_rate: DF.Float - price_list_currency: DF.Link | None - pricing_rules: DF.Table[PricingRuleDetail] - project: DF.Link | None - ref_sq: DF.Link | None - represents_company: DF.Link | None - rounded_total: DF.Currency - rounding_adjustment: DF.Currency - scan_barcode: DF.Data | None - schedule_date: DF.Date | None - select_print_heading: DF.Link | None - set_from_warehouse: DF.Link | None - set_reserve_warehouse: DF.Link | None - set_warehouse: DF.Link | None - shipping_address: DF.Link | None - shipping_address_display: DF.SmallText | None - shipping_rule: DF.Link | None - status: DF.Literal[ - "", - "Draft", - "On Hold", - "To Receive and Bill", - "To Bill", - "To Receive", - "Completed", - "Cancelled", - "Closed", - "Delivered", - ] - supplied_items: DF.Table[PurchaseOrderItemSupplied] - supplier: DF.Link - supplier_address: DF.Link | None - supplier_name: DF.Data | None - supplier_warehouse: DF.Link | None - tax_category: DF.Link | None - tax_withholding_category: DF.Link | None - tax_withholding_net_total: DF.Currency - taxes: DF.Table[PurchaseTaxesandCharges] - taxes_and_charges: DF.Link | None - taxes_and_charges_added: DF.Currency - taxes_and_charges_deducted: DF.Currency - tc_name: DF.Link | None - terms: DF.TextEditor | None - title: DF.Data - to_date: DF.Date | None - total: DF.Currency - total_net_weight: DF.Float - total_qty: DF.Float - total_taxes_and_charges: DF.Currency - transaction_date: DF.Date - # end: auto-generated types - ->>>>>>> 1c63983873 (fix: use Text Editor for rendering tax breakup table) def __init__(self, *args, **kwargs): super(PurchaseOrder, self).__init__(*args, **kwargs) self.status_updater = [ diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py index 70fea9135917..e27fbe8aaa23 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py @@ -14,98 +14,6 @@ class SupplierQuotation(BuyingController): -<<<<<<< HEAD -======= - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail - from erpnext.accounts.doctype.purchase_taxes_and_charges.purchase_taxes_and_charges import ( - PurchaseTaxesandCharges, - ) - from erpnext.buying.doctype.supplier_quotation_item.supplier_quotation_item import ( - SupplierQuotationItem, - ) - - additional_discount_percentage: DF.Float - address_display: DF.SmallText | None - amended_from: DF.Link | None - apply_discount_on: DF.Literal["", "Grand Total", "Net Total"] - auto_repeat: DF.Link | None - base_discount_amount: DF.Currency - base_grand_total: DF.Currency - base_in_words: DF.Data | None - base_net_total: DF.Currency - base_rounded_total: DF.Currency - base_rounding_adjustment: DF.Currency - base_taxes_and_charges_added: DF.Currency - base_taxes_and_charges_deducted: DF.Currency - base_total: DF.Currency - base_total_taxes_and_charges: DF.Currency - billing_address: DF.Link | None - billing_address_display: DF.SmallText | None - buying_price_list: DF.Link | None - company: DF.Link - contact_display: DF.SmallText | None - contact_email: DF.Data | None - contact_mobile: DF.SmallText | None - contact_person: DF.Link | None - conversion_rate: DF.Float - cost_center: DF.Link | None - currency: DF.Link - disable_rounded_total: DF.Check - discount_amount: DF.Currency - grand_total: DF.Currency - group_same_items: DF.Check - ignore_pricing_rule: DF.Check - in_words: DF.Data | None - incoterm: DF.Link | None - is_subcontracted: DF.Check - items: DF.Table[SupplierQuotationItem] - language: DF.Data | None - letter_head: DF.Link | None - named_place: DF.Data | None - naming_series: DF.Literal["PUR-SQTN-.YYYY.-"] - net_total: DF.Currency - opportunity: DF.Link | None - other_charges_calculation: DF.MarkdownEditor | None - plc_conversion_rate: DF.Float - price_list_currency: DF.Link | None - pricing_rules: DF.Table[PricingRuleDetail] - project: DF.Link | None - quotation_number: DF.Data | None - rounded_total: DF.Currency - rounding_adjustment: DF.Currency - select_print_heading: DF.Link | None - shipping_address: DF.Link | None - shipping_address_display: DF.SmallText | None - shipping_rule: DF.Link | None - status: DF.Literal["", "Draft", "Submitted", "Stopped", "Cancelled", "Expired"] - supplier: DF.Link - supplier_address: DF.Link | None - supplier_name: DF.Data | None - tax_category: DF.Link | None - taxes: DF.Table[PurchaseTaxesandCharges] - taxes_and_charges: DF.Link | None - taxes_and_charges_added: DF.Currency - taxes_and_charges_deducted: DF.Currency - tc_name: DF.Link | None - terms: DF.TextEditor | None - title: DF.Data | None - total: DF.Currency - total_net_weight: DF.Float - total_qty: DF.Float - total_taxes_and_charges: DF.Currency - transaction_date: DF.Date - valid_till: DF.Date | None - # end: auto-generated types - ->>>>>>> 1c63983873 (fix: use Text Editor for rendering tax breakup table) def validate(self): super(SupplierQuotation, self).validate() diff --git a/erpnext/public/scss/erpnext.scss b/erpnext/public/scss/erpnext.scss index 9d65166f8a7b..be3aed1ed0b4 100644 --- a/erpnext/public/scss/erpnext.scss +++ b/erpnext/public/scss/erpnext.scss @@ -496,62 +496,7 @@ body[data-route="pos"] { .exercise-col { padding: 10px; } -<<<<<<< HEAD -======= - -.plant-floor, -.workstation-wrapper, -.workstation-card p { - border-radius: var(--border-radius-md); - border: 1px solid var(--border-color); - box-shadow: none; - background-color: var(--card-bg); - position: relative; -} - -.plant-floor { - padding-bottom: 25px; -} - -.plant-floor-filter { - padding-top: 10px; - display: flex; - flex-wrap: wrap; -} - -.plant-floor-container { - display: grid; - grid-template-columns: repeat(6, minmax(0, 1fr)); - gap: var(--margin-xl); -} - -@media screen and (max-width: 620px) { - .plant-floor-container { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} - -.plant-floor-container .workstation-card { - padding: 5px; -} - -.plant-floor-container .workstation-image-link { - width: 100%; - font-size: 50px; - margin: var(--margin-sm); - min-height: 9rem; -} - -.workstation-abbr { - display: flex; - background-color: var(--control-bg); - height: 100%; - width: 100%; - align-items: center; - justify-content: center; -} .frappe-control[data-fieldname="other_charges_calculation"] .ql-editor { white-space: normal; } ->>>>>>> 967540da18 (fix: style for tax breakup) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index d09b1d30838a..de2cef909299 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -13,112 +13,6 @@ class Quotation(SellingController): -<<<<<<< HEAD -======= - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule - from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail - from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import ( - SalesTaxesandCharges, - ) - from erpnext.crm.doctype.competitor_detail.competitor_detail import CompetitorDetail - from erpnext.selling.doctype.quotation_item.quotation_item import QuotationItem - from erpnext.setup.doctype.quotation_lost_reason_detail.quotation_lost_reason_detail import ( - QuotationLostReasonDetail, - ) - from erpnext.stock.doctype.packed_item.packed_item import PackedItem - - additional_discount_percentage: DF.Float - address_display: DF.SmallText | None - amended_from: DF.Link | None - apply_discount_on: DF.Literal["", "Grand Total", "Net Total"] - auto_repeat: DF.Link | None - base_discount_amount: DF.Currency - base_grand_total: DF.Currency - base_in_words: DF.Data | None - base_net_total: DF.Currency - base_rounded_total: DF.Currency - base_rounding_adjustment: DF.Currency - base_total: DF.Currency - base_total_taxes_and_charges: DF.Currency - campaign: DF.Link | None - company: DF.Link - company_address: DF.Link | None - company_address_display: DF.SmallText | None - competitors: DF.TableMultiSelect[CompetitorDetail] - contact_display: DF.SmallText | None - contact_email: DF.Data | None - contact_mobile: DF.SmallText | None - contact_person: DF.Link | None - conversion_rate: DF.Float - coupon_code: DF.Link | None - currency: DF.Link - customer_address: DF.Link | None - customer_group: DF.Link | None - customer_name: DF.Data | None - discount_amount: DF.Currency - enq_det: DF.Text | None - grand_total: DF.Currency - group_same_items: DF.Check - ignore_pricing_rule: DF.Check - in_words: DF.Data | None - incoterm: DF.Link | None - items: DF.Table[QuotationItem] - language: DF.Data | None - letter_head: DF.Link | None - lost_reasons: DF.TableMultiSelect[QuotationLostReasonDetail] - named_place: DF.Data | None - naming_series: DF.Literal["SAL-QTN-.YYYY.-"] - net_total: DF.Currency - opportunity: DF.Link | None - order_lost_reason: DF.SmallText | None - order_type: DF.Literal["", "Sales", "Maintenance", "Shopping Cart"] - other_charges_calculation: DF.TextEditor | None - packed_items: DF.Table[PackedItem] - party_name: DF.DynamicLink | None - payment_schedule: DF.Table[PaymentSchedule] - payment_terms_template: DF.Link | None - plc_conversion_rate: DF.Float - price_list_currency: DF.Link - pricing_rules: DF.Table[PricingRuleDetail] - quotation_to: DF.Link - referral_sales_partner: DF.Link | None - rounded_total: DF.Currency - rounding_adjustment: DF.Currency - scan_barcode: DF.Data | None - select_print_heading: DF.Link | None - selling_price_list: DF.Link - shipping_address: DF.SmallText | None - shipping_address_name: DF.Link | None - shipping_rule: DF.Link | None - source: DF.Link | None - status: DF.Literal[ - "Draft", "Open", "Replied", "Partially Ordered", "Ordered", "Lost", "Cancelled", "Expired" - ] - supplier_quotation: DF.Link | None - tax_category: DF.Link | None - taxes: DF.Table[SalesTaxesandCharges] - taxes_and_charges: DF.Link | None - tc_name: DF.Link | None - terms: DF.TextEditor | None - territory: DF.Link | None - title: DF.Data | None - total: DF.Currency - total_net_weight: DF.Float - total_qty: DF.Float - total_taxes_and_charges: DF.Currency - transaction_date: DF.Date - valid_till: DF.Date | None - # end: auto-generated types - ->>>>>>> 1c63983873 (fix: use Text Editor for rendering tax breakup table) def set_indicator(self): if self.docstatus == 1: self.indicator_color = "blue" diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index da196c7ac4e0..81f9cedbc626 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -41,144 +41,6 @@ class WarehouseRequired(frappe.ValidationError): class SalesOrder(SellingController): -<<<<<<< HEAD -======= - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule - from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail - from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import ( - SalesTaxesandCharges, - ) - from erpnext.selling.doctype.sales_order_item.sales_order_item import SalesOrderItem - from erpnext.selling.doctype.sales_team.sales_team import SalesTeam - from erpnext.stock.doctype.packed_item.packed_item import PackedItem - - additional_discount_percentage: DF.Float - address_display: DF.SmallText | None - advance_paid: DF.Currency - advance_payment_status: DF.Literal["Not Requested", "Requested", "Partially Paid", "Fully Paid"] - amended_from: DF.Link | None - amount_eligible_for_commission: DF.Currency - apply_discount_on: DF.Literal["", "Grand Total", "Net Total"] - auto_repeat: DF.Link | None - base_discount_amount: DF.Currency - base_grand_total: DF.Currency - base_in_words: DF.Data | None - base_net_total: DF.Currency - base_rounded_total: DF.Currency - base_rounding_adjustment: DF.Currency - base_total: DF.Currency - base_total_taxes_and_charges: DF.Currency - billing_status: DF.Literal["Not Billed", "Fully Billed", "Partly Billed", "Closed"] - campaign: DF.Link | None - commission_rate: DF.Float - company: DF.Link - company_address: DF.Link | None - company_address_display: DF.SmallText | None - contact_display: DF.SmallText | None - contact_email: DF.Data | None - contact_mobile: DF.SmallText | None - contact_person: DF.Link | None - contact_phone: DF.Data | None - conversion_rate: DF.Float - cost_center: DF.Link | None - coupon_code: DF.Link | None - currency: DF.Link - customer: DF.Link - customer_address: DF.Link | None - customer_group: DF.Link | None - customer_name: DF.Data | None - delivery_date: DF.Date | None - delivery_status: DF.Literal[ - "Not Delivered", "Fully Delivered", "Partly Delivered", "Closed", "Not Applicable" - ] - disable_rounded_total: DF.Check - discount_amount: DF.Currency - dispatch_address: DF.SmallText | None - dispatch_address_name: DF.Link | None - from_date: DF.Date | None - grand_total: DF.Currency - group_same_items: DF.Check - ignore_pricing_rule: DF.Check - in_words: DF.Data | None - incoterm: DF.Link | None - inter_company_order_reference: DF.Link | None - is_internal_customer: DF.Check - items: DF.Table[SalesOrderItem] - language: DF.Data | None - letter_head: DF.Link | None - loyalty_amount: DF.Currency - loyalty_points: DF.Int - named_place: DF.Data | None - naming_series: DF.Literal["SAL-ORD-.YYYY.-"] - net_total: DF.Currency - order_type: DF.Literal["", "Sales", "Maintenance", "Shopping Cart"] - other_charges_calculation: DF.TextEditor | None - packed_items: DF.Table[PackedItem] - party_account_currency: DF.Link | None - payment_schedule: DF.Table[PaymentSchedule] - payment_terms_template: DF.Link | None - per_billed: DF.Percent - per_delivered: DF.Percent - per_picked: DF.Percent - plc_conversion_rate: DF.Float - po_date: DF.Date | None - po_no: DF.Data | None - price_list_currency: DF.Link - pricing_rules: DF.Table[PricingRuleDetail] - project: DF.Link | None - represents_company: DF.Link | None - reserve_stock: DF.Check - rounded_total: DF.Currency - rounding_adjustment: DF.Currency - sales_partner: DF.Link | None - sales_team: DF.Table[SalesTeam] - scan_barcode: DF.Data | None - select_print_heading: DF.Link | None - selling_price_list: DF.Link - set_warehouse: DF.Link | None - shipping_address: DF.SmallText | None - shipping_address_name: DF.Link | None - shipping_rule: DF.Link | None - skip_delivery_note: DF.Check - source: DF.Link | None - status: DF.Literal[ - "", - "Draft", - "On Hold", - "To Pay", - "To Deliver and Bill", - "To Bill", - "To Deliver", - "Completed", - "Cancelled", - "Closed", - ] - tax_category: DF.Link | None - tax_id: DF.Data | None - taxes: DF.Table[SalesTaxesandCharges] - taxes_and_charges: DF.Link | None - tc_name: DF.Link | None - terms: DF.TextEditor | None - territory: DF.Link | None - title: DF.Data | None - to_date: DF.Date | None - total: DF.Currency - total_commission: DF.Currency - total_net_weight: DF.Float - total_qty: DF.Float - total_taxes_and_charges: DF.Currency - transaction_date: DF.Date - # end: auto-generated types - ->>>>>>> 1c63983873 (fix: use Text Editor for rendering tax breakup table) def __init__(self, *args, **kwargs): super(SalesOrder, self).__init__(*args, **kwargs) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 86e07bcd2624..a402bb5aed28 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -19,131 +19,6 @@ class DeliveryNote(SellingController): -<<<<<<< HEAD -======= - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail - from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import ( - SalesTaxesandCharges, - ) - from erpnext.selling.doctype.sales_team.sales_team import SalesTeam - from erpnext.stock.doctype.delivery_note_item.delivery_note_item import DeliveryNoteItem - from erpnext.stock.doctype.packed_item.packed_item import PackedItem - - additional_discount_percentage: DF.Float - address_display: DF.SmallText | None - amended_from: DF.Link | None - amount_eligible_for_commission: DF.Currency - apply_discount_on: DF.Literal["", "Grand Total", "Net Total"] - auto_repeat: DF.Link | None - base_discount_amount: DF.Currency - base_grand_total: DF.Currency - base_in_words: DF.Data | None - base_net_total: DF.Currency - base_rounded_total: DF.Currency - base_rounding_adjustment: DF.Currency - base_total: DF.Currency - base_total_taxes_and_charges: DF.Currency - campaign: DF.Link | None - commission_rate: DF.Float - company: DF.Link - company_address: DF.Link | None - company_address_display: DF.SmallText | None - contact_display: DF.SmallText | None - contact_email: DF.Data | None - contact_mobile: DF.SmallText | None - contact_person: DF.Link | None - conversion_rate: DF.Float - cost_center: DF.Link | None - currency: DF.Link - customer: DF.Link - customer_address: DF.Link | None - customer_group: DF.Link | None - customer_name: DF.Data | None - disable_rounded_total: DF.Check - discount_amount: DF.Currency - dispatch_address: DF.SmallText | None - dispatch_address_name: DF.Link | None - driver: DF.Link | None - driver_name: DF.Data | None - excise_page: DF.Data | None - grand_total: DF.Currency - group_same_items: DF.Check - ignore_pricing_rule: DF.Check - in_words: DF.Data | None - incoterm: DF.Link | None - installation_status: DF.Literal[None] - instructions: DF.Text | None - inter_company_reference: DF.Link | None - is_internal_customer: DF.Check - is_return: DF.Check - issue_credit_note: DF.Check - items: DF.Table[DeliveryNoteItem] - language: DF.Data | None - letter_head: DF.Link | None - lr_date: DF.Date | None - lr_no: DF.Data | None - named_place: DF.Data | None - naming_series: DF.Literal["MAT-DN-.YYYY.-", "MAT-DN-RET-.YYYY.-"] - net_total: DF.Currency - other_charges_calculation: DF.TextEditor | None - packed_items: DF.Table[PackedItem] - per_billed: DF.Percent - per_installed: DF.Percent - per_returned: DF.Percent - pick_list: DF.Link | None - plc_conversion_rate: DF.Float - po_date: DF.Date | None - po_no: DF.SmallText | None - posting_date: DF.Date - posting_time: DF.Time - price_list_currency: DF.Link - pricing_rules: DF.Table[PricingRuleDetail] - print_without_amount: DF.Check - project: DF.Link | None - represents_company: DF.Link | None - return_against: DF.Link | None - rounded_total: DF.Currency - rounding_adjustment: DF.Currency - sales_partner: DF.Link | None - sales_team: DF.Table[SalesTeam] - scan_barcode: DF.Data | None - select_print_heading: DF.Link | None - selling_price_list: DF.Link - set_posting_time: DF.Check - set_target_warehouse: DF.Link | None - set_warehouse: DF.Link | None - shipping_address: DF.SmallText | None - shipping_address_name: DF.Link | None - shipping_rule: DF.Link | None - source: DF.Link | None - status: DF.Literal["", "Draft", "To Bill", "Completed", "Return Issued", "Cancelled", "Closed"] - tax_category: DF.Link | None - tax_id: DF.Data | None - taxes: DF.Table[SalesTaxesandCharges] - taxes_and_charges: DF.Link | None - tc_name: DF.Link | None - terms: DF.TextEditor | None - territory: DF.Link | None - title: DF.Data | None - total: DF.Currency - total_commission: DF.Currency - total_net_weight: DF.Float - total_qty: DF.Float - total_taxes_and_charges: DF.Currency - transporter: DF.Link | None - transporter_name: DF.Data | None - vehicle_no: DF.Data | None - # end: auto-generated types - ->>>>>>> 1c63983873 (fix: use Text Editor for rendering tax breakup table) def __init__(self, *args, **kwargs): super(DeliveryNote, self).__init__(*args, **kwargs) self.status_updater = [ diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index e58fca996439..79e6ab84d950 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -21,121 +21,6 @@ class PurchaseReceipt(BuyingController): -<<<<<<< HEAD -======= - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail - from erpnext.accounts.doctype.purchase_taxes_and_charges.purchase_taxes_and_charges import ( - PurchaseTaxesandCharges, - ) - from erpnext.buying.doctype.purchase_receipt_item_supplied.purchase_receipt_item_supplied import ( - PurchaseReceiptItemSupplied, - ) - from erpnext.stock.doctype.purchase_receipt_item.purchase_receipt_item import PurchaseReceiptItem - - additional_discount_percentage: DF.Float - address_display: DF.SmallText | None - amended_from: DF.Link | None - apply_discount_on: DF.Literal["", "Grand Total", "Net Total"] - apply_putaway_rule: DF.Check - auto_repeat: DF.Link | None - base_discount_amount: DF.Currency - base_grand_total: DF.Currency - base_in_words: DF.Data | None - base_net_total: DF.Currency - base_rounded_total: DF.Currency - base_rounding_adjustment: DF.Currency - base_taxes_and_charges_added: DF.Currency - base_taxes_and_charges_deducted: DF.Currency - base_total: DF.Currency - base_total_taxes_and_charges: DF.Currency - billing_address: DF.Link | None - billing_address_display: DF.SmallText | None - buying_price_list: DF.Link | None - company: DF.Link - contact_display: DF.SmallText | None - contact_email: DF.SmallText | None - contact_mobile: DF.SmallText | None - contact_person: DF.Link | None - conversion_rate: DF.Float - cost_center: DF.Link | None - currency: DF.Link - disable_rounded_total: DF.Check - discount_amount: DF.Currency - grand_total: DF.Currency - group_same_items: DF.Check - ignore_pricing_rule: DF.Check - in_words: DF.Data | None - incoterm: DF.Link | None - instructions: DF.SmallText | None - inter_company_reference: DF.Link | None - is_internal_supplier: DF.Check - is_old_subcontracting_flow: DF.Check - is_return: DF.Check - is_subcontracted: DF.Check - items: DF.Table[PurchaseReceiptItem] - language: DF.Data | None - letter_head: DF.Link | None - lr_date: DF.Date | None - lr_no: DF.Data | None - named_place: DF.Data | None - naming_series: DF.Literal["MAT-PRE-.YYYY.-", "MAT-PR-RET-.YYYY.-"] - net_total: DF.Currency - other_charges_calculation: DF.TextEditor | None - per_billed: DF.Percent - per_returned: DF.Percent - plc_conversion_rate: DF.Float - posting_date: DF.Date - posting_time: DF.Time - price_list_currency: DF.Link | None - pricing_rules: DF.Table[PricingRuleDetail] - project: DF.Link | None - range: DF.Data | None - rejected_warehouse: DF.Link | None - remarks: DF.SmallText | None - represents_company: DF.Link | None - return_against: DF.Link | None - rounded_total: DF.Currency - rounding_adjustment: DF.Currency - scan_barcode: DF.Data | None - select_print_heading: DF.Link | None - set_from_warehouse: DF.Link | None - set_posting_time: DF.Check - set_warehouse: DF.Link | None - shipping_address: DF.Link | None - shipping_address_display: DF.SmallText | None - shipping_rule: DF.Link | None - status: DF.Literal["", "Draft", "To Bill", "Completed", "Return Issued", "Cancelled", "Closed"] - subcontracting_receipt: DF.Link | None - supplied_items: DF.Table[PurchaseReceiptItemSupplied] - supplier: DF.Link - supplier_address: DF.Link | None - supplier_delivery_note: DF.Data | None - supplier_name: DF.Data | None - supplier_warehouse: DF.Link | None - tax_category: DF.Link | None - taxes: DF.Table[PurchaseTaxesandCharges] - taxes_and_charges: DF.Link | None - taxes_and_charges_added: DF.Currency - taxes_and_charges_deducted: DF.Currency - tc_name: DF.Link | None - terms: DF.TextEditor | None - title: DF.Data | None - total: DF.Currency - total_net_weight: DF.Float - total_qty: DF.Float - total_taxes_and_charges: DF.Currency - transporter_name: DF.Data | None - # end: auto-generated types - ->>>>>>> 1c63983873 (fix: use Text Editor for rendering tax breakup table) def __init__(self, *args, **kwargs): super(PurchaseReceipt, self).__init__(*args, **kwargs) self.status_updater = [ From 1df54114a87b665148ccfdcf294eae2ec501f605 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 20 Mar 2024 20:20:44 +0530 Subject: [PATCH 48/94] chore: fixed test case --- erpnext/stock/stock_ledger.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index a642a1ff7ed8..96a554de72b3 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1340,9 +1340,15 @@ def get_batch_incoming_rate( item_code, warehouse, batch_no, posting_date, posting_time, creation=None ): + import datetime + sle = frappe.qb.DocType("Stock Ledger Entry") - timestamp_condition = sle.posting_datetime < get_combine_datetime(posting_date, posting_time) + posting_datetime = get_combine_datetime(posting_date, posting_time) + if not creation: + posting_datetime = posting_datetime + datetime.timedelta(milliseconds=1) + + timestamp_condition = sle.posting_datetime < posting_datetime if creation: timestamp_condition |= ( sle.posting_datetime == get_combine_datetime(posting_date, posting_time) From 1515bb7f0b90d1a011e3705e5de83928351be5cf Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 21 Mar 2024 09:53:55 +0530 Subject: [PATCH 49/94] refactor: replace get_job with create_job_id utility method --- .../transaction_deletion_record.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index f1eb4e6a9b93..db5024bbc199 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -8,7 +8,7 @@ from frappe.desk.notifications import clear_notifications from frappe.model.document import Document from frappe.utils import cint, comma_and, create_batch, get_link_to_form -from frappe.utils.background_jobs import get_job, is_job_enqueued +from frappe.utils.background_jobs import create_job_id, is_job_enqueued class TransactionDeletionRecord(Document): @@ -152,7 +152,7 @@ def validate_running_task_for_doc(self, job_names: list = None): running_tasks = [] for x in job_names: if is_job_enqueued(x): - running_tasks.append(get_job(x).get_id()) + running_tasks.append(create_job_id(x)) if running_tasks: frappe.throw( From 5791c50fdf4676019645b6398c4bcbc3e9669858 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 21 Mar 2024 10:29:56 +0530 Subject: [PATCH 50/94] refactor: config changes in Transaction Deletion Record (cherry picked from commit 4ba67fb3ec9668dc743e24aad43ed17f286f48de) --- .../transaction_deletion_record.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json index e03e1695e0ef..b9f911dbe8c6 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json @@ -135,13 +135,16 @@ "default": "0", "fieldname": "process_in_single_transaction", "fieldtype": "Check", - "label": "Process in Single Transaction" + "hidden": 1, + "label": "Process in Single Transaction", + "no_copy": 1, + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-03-20 14:58:15.086360", + "modified": "2024-03-21 10:29:19.456413", "modified_by": "Administrator", "module": "Setup", "name": "Transaction Deletion Record", From 82b613353bfcbfbcb436aec847986cb1773c74bc Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 20 Mar 2024 18:45:04 +0530 Subject: [PATCH 51/94] fix: validate gl for previous fiscal year (cherry picked from commit a1d108c062831b2301b2b62b09dad35e8bbd1959) --- .../doctype/period_closing_voucher/period_closing_voucher.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index 674db6c2e430..88a2ca575ac5 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -121,7 +121,8 @@ def check_if_previous_year_closed(self): previous_fiscal_year = get_fiscal_year(last_year_closing, company=self.company, boolean=True) if previous_fiscal_year and not frappe.db.exists( - "GL Entry", {"posting_date": ("<=", last_year_closing), "company": self.company} + "GL Entry", + {"posting_date": ("<=", last_year_closing), "company": self.company, "is_cancelled": 0}, ): return From 2b311131ba9346dfd4a0246d6d2261be224631c6 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 12 Mar 2024 12:52:19 +0530 Subject: [PATCH 52/94] fix: wrong buying amount if delivered and billed qty varies (cherry picked from commit b8da0d9334919e283724f2e02ce3dd3a9780d9a9) --- erpnext/accounts/report/gross_profit/gross_profit.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index de3d57d095a8..162218df1a52 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -669,20 +669,20 @@ def get_buying_amount(self, row, item_code): elif row.sales_order and row.so_detail: incoming_amount = self.get_buying_amount_from_so_dn(row.sales_order, row.so_detail, item_code) if incoming_amount: - return incoming_amount + return flt(row.qty) * incoming_amount else: return flt(row.qty) * self.get_average_buying_rate(row, item_code) return flt(row.qty) * self.get_average_buying_rate(row, item_code) def get_buying_amount_from_so_dn(self, sales_order, so_detail, item_code): - from frappe.query_builder.functions import Sum + from frappe.query_builder.functions import Avg delivery_note_item = frappe.qb.DocType("Delivery Note Item") query = ( frappe.qb.from_(delivery_note_item) - .select(Sum(delivery_note_item.incoming_rate * delivery_note_item.stock_qty)) + .select(Avg(delivery_note_item.incoming_rate)) .where(delivery_note_item.docstatus == 1) .where(delivery_note_item.item_code == item_code) .where(delivery_note_item.against_sales_order == sales_order) From d3a6153077084b02155acfd28229dca4900dc5a6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 21 Mar 2024 20:56:52 +0530 Subject: [PATCH 53/94] test: buying amt calculation if DN and SI differ in qty (cherry picked from commit ccb51ded957fb4fd16f4dd7ce7c7e285f6ef1a81) --- .../report/gross_profit/test_gross_profit.py | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/erpnext/accounts/report/gross_profit/test_gross_profit.py b/erpnext/accounts/report/gross_profit/test_gross_profit.py index 82fe1a0ba129..aa820aa4c731 100644 --- a/erpnext/accounts/report/gross_profit/test_gross_profit.py +++ b/erpnext/accounts/report/gross_profit/test_gross_profit.py @@ -460,3 +460,95 @@ def test_standalone_cr_notes(self): } gp_entry = [x for x in data if x.parent_invoice == sinv.name] self.assertDictContainsSubset(expected_entry, gp_entry[0]) + + def test_different_rates_in_si_and_dn(self): + from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order + + """ + Test gp calculation when invoice and delivery note differ in qty and aren't connected + SO -- INV + | + DN + """ + se = make_stock_entry( + company=self.company, + item_code=self.item, + target=self.warehouse, + qty=3, + basic_rate=700, + do_not_submit=True, + ) + item = se.items[0] + se.append( + "items", + { + "item_code": item.item_code, + "s_warehouse": item.s_warehouse, + "t_warehouse": item.t_warehouse, + "qty": 10, + "basic_rate": 700, + "conversion_factor": item.conversion_factor or 1.0, + "transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0), + "serial_no": item.serial_no, + "batch_no": item.batch_no, + "cost_center": item.cost_center, + "expense_account": item.expense_account, + }, + ) + se = se.save().submit() + + so = make_sales_order( + customer=self.customer, + company=self.company, + warehouse=self.warehouse, + item=self.item, + rate=800, + qty=10, + do_not_save=False, + do_not_submit=False, + ) + + from erpnext.selling.doctype.sales_order.sales_order import ( + make_delivery_note, + make_sales_invoice, + ) + + dn1 = make_delivery_note(so.name) + dn1.items[0].qty = 4 + dn1.items[0].rate = 800 + dn1.save().submit() + + dn2 = make_delivery_note(so.name) + dn2.items[0].qty = 6 + dn2.items[0].rate = 800 + dn2.save().submit() + + sinv = make_sales_invoice(so.name) + sinv.items[0].qty = 4 + sinv.items[0].rate = 800 + sinv.save().submit() + + filters = frappe._dict( + company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice" + ) + + columns, data = execute(filters=filters) + expected_entry = { + "parent_invoice": sinv.name, + "currency": "INR", + "sales_invoice": self.item, + "customer": self.customer, + "posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()), + "item_code": self.item, + "item_name": self.item, + "warehouse": "Stores - _GP", + "qty": 4.0, + "avg._selling_rate": 800.0, + "valuation_rate": 700.0, + "selling_amount": 3200.0, + "buying_amount": 2800.0, + "gross_profit": 400.0, + "gross_profit_%": 12.5, + } + gp_entry = [x for x in data if x.parent_invoice == sinv.name] + self.assertDictContainsSubset(expected_entry, gp_entry[0]) From 9b3c4ac5757d049b6ed5f48a9f13572b383eba09 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 21 Mar 2024 21:04:03 +0530 Subject: [PATCH 54/94] fix: rate not fetching from the item price (cherry picked from commit d893a465d79b2e301b49b093f05d21d9eb196c9d) --- erpnext/stock/get_item_details.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 289a654ac12a..aa2878727b08 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -859,7 +859,6 @@ def get_price_list_rate(args, item_doc, out=None): ): if args.price_list and args.rate: insert_item_price(args) - return out out.price_list_rate = ( flt(price_list_rate) * flt(args.plc_conversion_rate) / flt(args.conversion_rate) From 390d3a6a9fdfbe35ed1cfcd8c6cfecdde97c7d36 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 22 Mar 2024 08:51:33 +0530 Subject: [PATCH 55/94] perf: add in some indexes (backport #40590) (#40604) * perf: add in some indexes (#40590) `Sales Invoice Item.purchase_order` `Delivery Note Item.purchase_order` Signed-off-by: Akhil Narang (cherry picked from commit 1cd38c860a5da2ab33d4da0a5f29bf95c788e59a) # Conflicts: # erpnext/selling/doctype/sales_order_item/sales_order_item.json # erpnext/stock/doctype/delivery_note_item/delivery_note_item.json * chore: `conflicts` * chore: `conflicts` --------- Co-authored-by: Akhil Narang Co-authored-by: s-aga-r --- .../selling/doctype/sales_order_item/sales_order_item.json | 5 +++-- .../stock/doctype/delivery_note_item/delivery_note_item.json | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index 9797b6ae11f9..110d80253d3a 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -832,7 +832,8 @@ "label": "Purchase Order", "options": "Purchase Order", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "column_break_89", @@ -875,7 +876,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-11-24 19:07:17.715231", + "modified": "2024-03-21 18:15:56.625005", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index b85bfe5036d8..25788966780a 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -809,7 +809,8 @@ "label": "Purchase Order", "options": "Purchase Order", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "column_break_82", @@ -870,7 +871,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:37:38.638144", + "modified": "2024-03-21 18:15:07.603672", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", From 99faafb5be7ee8faef64c549a0ac47a3208a0901 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 18 Mar 2024 23:05:58 +0100 Subject: [PATCH 56/94] fix: permissions during bulk transaction logs (cherry picked from commit 49dd4c1ef3fcb6f710c185c50188d0bee4ea2dcd) --- erpnext/utilities/bulk_transaction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/utilities/bulk_transaction.py b/erpnext/utilities/bulk_transaction.py index b519617435fc..17146e50d674 100644 --- a/erpnext/utilities/bulk_transaction.py +++ b/erpnext/utilities/bulk_transaction.py @@ -162,7 +162,7 @@ def create_log(doc_name, e, from_doctype, to_doctype, status, log_date=None, res transaction_log.from_doctype = from_doctype transaction_log.to_doctype = to_doctype transaction_log.retried = restarted - transaction_log.save() + transaction_log.save(ignore_permissions=True) def show_job_status(fail_count, deserialized_data_count, to_doctype): From 9e15ecfe34a520d9dc5824d0c6985beb98656ca2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 15 Sep 2023 21:24:53 +0530 Subject: [PATCH 57/94] refactor: primitive summary for p&l and balance sheet (cherry picked from commit eb4c476490ea32cd94e0b6cc3715abe99f1d4e0a) --- erpnext/accounts/report/balance_sheet/balance_sheet.py | 6 +++--- .../profit_and_loss_statement/profit_and_loss_statement.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.py b/erpnext/accounts/report/balance_sheet/balance_sheet.py index b225aac7b560..5d6ca23a6b2e 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.py +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.py @@ -97,11 +97,11 @@ def execute(filters=None): chart = get_chart_data(filters, columns, asset, liability, equity) - report_summary = get_report_summary( + report_summary, primitive_summary = get_report_summary( period_list, asset, liability, equity, provisional_profit_loss, currency, filters ) - return columns, data, message, chart, report_summary + return columns, data, message, chart, report_summary, primitive_summary def get_provisional_profit_loss( @@ -217,7 +217,7 @@ def get_report_summary( "datatype": "Currency", "currency": currency, }, - ] + ], (net_asset - net_liability + net_equity) def get_chart_data(filters, columns, asset, liability, equity): diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py index 66353358a062..002c05c9e3f6 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py @@ -66,11 +66,11 @@ def execute(filters=None): currency = filters.presentation_currency or frappe.get_cached_value( "Company", filters.company, "default_currency" ) - report_summary = get_report_summary( + report_summary, primitive_summary = get_report_summary( period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters ) - return columns, data, None, chart, report_summary + return columns, data, None, chart, report_summary, primitive_summary def get_report_summary( @@ -112,7 +112,7 @@ def get_report_summary( "datatype": "Currency", "currency": currency, }, - ] + ], net_profit def get_net_profit_loss(income, expense, period_list, company, currency=None, consolidated=False): From 5ad39182321cdab8fe211adb2b5f28957260ab3b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 15 Sep 2023 21:38:15 +0530 Subject: [PATCH 58/94] feat: bisect doctype (cherry picked from commit decdbd278200bb83baa7c5f08bd706c1c7c5406a) --- .../bisect_accounting_statements/__init__.py | 0 .../bisect_accounting_statements.js | 8 +++ .../bisect_accounting_statements.json | 54 +++++++++++++++++++ .../bisect_accounting_statements.py | 9 ++++ .../test_bisect_accounting_statements.py | 9 ++++ 5 files changed, 80 insertions(+) create mode 100644 erpnext/accounts/doctype/bisect_accounting_statements/__init__.py create mode 100644 erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js create mode 100644 erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json create mode 100644 erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py create mode 100644 erpnext/accounts/doctype/bisect_accounting_statements/test_bisect_accounting_statements.py diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/__init__.py b/erpnext/accounts/doctype/bisect_accounting_statements/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js new file mode 100644 index 000000000000..7e1fb48fe3c9 --- /dev/null +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js @@ -0,0 +1,8 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Bisect Accounting Statements", { +// refresh(frm) { + +// }, +// }); diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json new file mode 100644 index 000000000000..1ffa3adcb3d6 --- /dev/null +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json @@ -0,0 +1,54 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-09-15 21:28:28.054773", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "from", + "column_break_qxbi", + "to" + ], + "fields": [ + { + "fieldname": "from", + "fieldtype": "Date", + "label": "From" + }, + { + "fieldname": "to", + "fieldtype": "Date", + "label": "To" + }, + { + "fieldname": "column_break_qxbi", + "fieldtype": "Column Break" + } + ], + "hide_toolbar": 1, + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2023-09-15 21:36:21.516679", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Bisect Accounting Statements", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py new file mode 100644 index 000000000000..8ab97b47a863 --- /dev/null +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class BisectAccountingStatements(Document): + pass diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/test_bisect_accounting_statements.py b/erpnext/accounts/doctype/bisect_accounting_statements/test_bisect_accounting_statements.py new file mode 100644 index 000000000000..56ecc94a18e7 --- /dev/null +++ b/erpnext/accounts/doctype/bisect_accounting_statements/test_bisect_accounting_statements.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestBisectAccountingStatements(FrappeTestCase): + pass From 57ec0d6b419121ac51a762c29a4ca397771ad67a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 18 Sep 2023 12:35:24 +0530 Subject: [PATCH 59/94] refactor: some logic (cherry picked from commit 4c8a8c3bcd9bf21918a78de244789c7de76cede9) --- .../bisect_accounting_statements.js | 22 ++++++++--- .../bisect_accounting_statements.json | 38 +++++++++++++++---- .../bisect_accounting_statements.py | 17 ++++++++- 3 files changed, 62 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js index 7e1fb48fe3c9..96b9709beedf 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js @@ -1,8 +1,20 @@ // Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -// frappe.ui.form.on("Bisect Accounting Statements", { -// refresh(frm) { - -// }, -// }); +frappe.ui.form.on("Bisect Accounting Statements", { + refresh(frm) { + frm.add_custom_button(__('Bisect'), () => + frm.trigger("bisect") + ); + frm.change_custom_button_type(__('Bisect'), null, 'primary'); + }, + bisect(frm) { + frm.call({ + doc: frm.doc, + method: 'bisect', + callback: (r) => { + console.log(r); + } + }); + } +}); diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json index 1ffa3adcb3d6..de7eba7c63cc 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json @@ -7,31 +7,53 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "from", + "from_date", "column_break_qxbi", - "to" + "to_date", + "section_break_3x70", + "period_from", + "column_break_5ett", + "period_to" ], "fields": [ { - "fieldname": "from", + "fieldname": "column_break_qxbi", + "fieldtype": "Column Break" + }, + { + "fieldname": "from_date", "fieldtype": "Date", - "label": "From" + "label": "From Date" }, { - "fieldname": "to", + "fieldname": "to_date", "fieldtype": "Date", - "label": "To" + "label": "To Date" }, { - "fieldname": "column_break_qxbi", + "fieldname": "section_break_3x70", + "fieldtype": "Section Break" + }, + { + "fieldname": "period_from", + "fieldtype": "Date", + "label": "Period From" + }, + { + "fieldname": "column_break_5ett", "fieldtype": "Column Break" + }, + { + "fieldname": "period_to", + "fieldtype": "Date", + "label": "Period To" } ], "hide_toolbar": 1, "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-09-15 21:36:21.516679", + "modified": "2023-09-16 08:02:33.472406", "modified_by": "Administrator", "module": "Accounts", "name": "Bisect Accounting Statements", diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py index 8ab97b47a863..2e209d2a6373 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py @@ -1,9 +1,22 @@ # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -# import frappe +from math import ceil, floor + +import frappe +from dateutil.relativedelta import relativedelta from frappe.model.document import Document +from frappe.utils import getdate class BisectAccountingStatements(Document): - pass + @frappe.whitelist() + def bisect(self): + cur_frm_date, cur_to_date = getdate(self.from_date), getdate(self.to_date) + while True: + delta = cur_to_date - cur_frm_date + if delta.days == 0: + return + cur_floor = floor(delta.days / 2) + cur_to_date = cur_frm_date + relativedelta(days=+cur_floor) + print((cur_frm_date, cur_to_date), delta, cur_floor) From 163997494831f489761d0e10c8fef73c69feb431 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 25 Sep 2023 10:35:54 +0530 Subject: [PATCH 60/94] refactor: Depth First Search(DFS) (cherry picked from commit 5a25c80f2eb434313b65b1b3b08bd0ea5e393461) --- .../bisect_accounting_statements.py | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py index 2e209d2a6373..6be28c87bc6b 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py @@ -1,7 +1,7 @@ # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from math import ceil, floor +from math import floor import frappe from dateutil.relativedelta import relativedelta @@ -12,11 +12,18 @@ class BisectAccountingStatements(Document): @frappe.whitelist() def bisect(self): - cur_frm_date, cur_to_date = getdate(self.from_date), getdate(self.to_date) - while True: + period_list = [(getdate(self.from_date), getdate(self.to_date))] + dates = [] + while period_list: + cur_frm_date, cur_to_date = period_list.pop() delta = cur_to_date - cur_frm_date - if delta.days == 0: - return + if not delta.days > 0: + continue + cur_floor = floor(delta.days / 2) - cur_to_date = cur_frm_date + relativedelta(days=+cur_floor) - print((cur_frm_date, cur_to_date), delta, cur_floor) + next_to_date = cur_frm_date + relativedelta(days=+cur_floor) + left = (cur_frm_date, next_to_date) + period_list.append(left) + next_frm_date = cur_frm_date + relativedelta(days=+(cur_floor + 1)) + right = (next_frm_date, cur_to_date) + period_list.append(right) From 50963d9ad5b53bff2875b6cf8a1f1f05a9f38114 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 25 Sep 2023 10:39:39 +0530 Subject: [PATCH 61/94] refactor: simplify DFS (cherry picked from commit 26503a205f07a80f124c3dc5a59c039ed8ea708b) --- .../bisect_accounting_statements.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py index 6be28c87bc6b..73a9e7196098 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py @@ -21,9 +21,7 @@ def bisect(self): continue cur_floor = floor(delta.days / 2) - next_to_date = cur_frm_date + relativedelta(days=+cur_floor) - left = (cur_frm_date, next_to_date) + left = (cur_frm_date, (cur_frm_date + relativedelta(days=+cur_floor))) + right = ((cur_frm_date + relativedelta(days=+(cur_floor + 1))), cur_to_date) period_list.append(left) - next_frm_date = cur_frm_date + relativedelta(days=+(cur_floor + 1)) - right = (next_frm_date, cur_to_date) period_list.append(right) From 0399acdc8e204c6fd927bfa5f40e16efb229524d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 25 Sep 2023 11:05:30 +0530 Subject: [PATCH 62/94] refactor: support for BFS and DFS (cherry picked from commit 03a38ed0257cdee5ae1ec645877691ed1292ba9c) --- .../bisect_accounting_statements.json | 27 ++++------ .../bisect_accounting_statements.py | 50 +++++++++++++++---- 2 files changed, 50 insertions(+), 27 deletions(-) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json index de7eba7c63cc..b2f3c4bb90be 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json @@ -10,10 +10,8 @@ "from_date", "column_break_qxbi", "to_date", - "section_break_3x70", - "period_from", - "column_break_5ett", - "period_to" + "column_break_iwny", + "algorithm" ], "fields": [ { @@ -31,29 +29,22 @@ "label": "To Date" }, { - "fieldname": "section_break_3x70", - "fieldtype": "Section Break" + "default": "BFS", + "fieldname": "algorithm", + "fieldtype": "Select", + "label": "Algorithm", + "options": "BFS\nDFS" }, { - "fieldname": "period_from", - "fieldtype": "Date", - "label": "Period From" - }, - { - "fieldname": "column_break_5ett", + "fieldname": "column_break_iwny", "fieldtype": "Column Break" - }, - { - "fieldname": "period_to", - "fieldtype": "Date", - "label": "Period To" } ], "hide_toolbar": 1, "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-09-16 08:02:33.472406", + "modified": "2023-09-25 10:50:53.887235", "modified_by": "Administrator", "module": "Accounts", "name": "Bisect Accounting Statements", diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py index 73a9e7196098..bdd18095359a 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py @@ -1,6 +1,7 @@ # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +from collections import deque from math import floor import frappe @@ -10,18 +11,49 @@ class BisectAccountingStatements(Document): - @frappe.whitelist() - def bisect(self): - period_list = [(getdate(self.from_date), getdate(self.to_date))] + def bfs(self): + period_list = deque([(getdate(self.from_date), getdate(self.to_date))]) + periods = [] dates = [] + while period_list: + cur_frm_date, cur_to_date = period_list.popleft() + delta = cur_to_date - cur_frm_date + periods.append((cur_frm_date, cur_to_date, delta)) + if delta.days == 0: + continue + else: + cur_floor = floor(delta.days / 2) + left = (cur_frm_date, (cur_frm_date + relativedelta(days=+cur_floor))) + right = ((cur_frm_date + relativedelta(days=+(cur_floor + 1))), cur_to_date) + period_list.append(left) + period_list.append(right) + return periods + + def dfs(self): + period_list = [(getdate(self.from_date), getdate(self.to_date))] + periods = [] while period_list: cur_frm_date, cur_to_date = period_list.pop() delta = cur_to_date - cur_frm_date - if not delta.days > 0: + periods.append((cur_frm_date, cur_to_date, delta)) + if delta.days == 0: continue + else: + cur_floor = floor(delta.days / 2) + left = (cur_frm_date, (cur_frm_date + relativedelta(days=+cur_floor))) + right = ((cur_frm_date + relativedelta(days=+(cur_floor + 1))), cur_to_date) + period_list.append(left) + period_list.append(right) + return periods + + @frappe.whitelist() + def bisect(self): + if self.algorithm == "BFS": + periods = self.bfs() + + if self.algorithm == "DFS": + periods = self.dfs() - cur_floor = floor(delta.days / 2) - left = (cur_frm_date, (cur_frm_date + relativedelta(days=+cur_floor))) - right = ((cur_frm_date + relativedelta(days=+(cur_floor + 1))), cur_to_date) - period_list.append(left) - period_list.append(right) + print("Periods: ", len(periods)) + for x in periods: + print(x) From d81be8d8558142e32409efa5371f860121ac9e49 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 25 Sep 2023 11:13:50 +0530 Subject: [PATCH 63/94] refactor: date validation (cherry picked from commit 2de3e6ce6d38cfc8b520365de79044d616fbd24b) --- .../bisect_accounting_statements.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py index bdd18095359a..e032083f9033 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py @@ -6,6 +6,7 @@ import frappe from dateutil.relativedelta import relativedelta +from frappe import _ from frappe.model.document import Document from frappe.utils import getdate @@ -57,3 +58,14 @@ def bisect(self): print("Periods: ", len(periods)) for x in periods: print(x) + + def validate(self): + self.validate_dates() + + def validate_dates(self): + if getdate(self.from_date) > getdate(self.to_date): + frappe.throw( + _("From Date: {0} cannot be greater than To date: {1}").format( + frappe.bold(self.from_date), frappe.bold(self.to_date) + ) + ) From a177137d6d37f73b8b3d8c2679ce3e47a8430b2c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 25 Sep 2023 12:16:27 +0530 Subject: [PATCH 64/94] refactor: more buttons (cherry picked from commit a4270291511954b0e68fc126626967f4939c7617) --- .../bisect_accounting_statements.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js index 96b9709beedf..69bd56e73f29 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js @@ -3,10 +3,21 @@ frappe.ui.form.on("Bisect Accounting Statements", { refresh(frm) { + frm.add_custom_button(__('Bisect Left'), () => + frm.trigger("bisect_left") + ); + + frm.add_custom_button(__('Bisect Right'), () => + frm.trigger("bisect_right") + ); + + frm.add_custom_button(__('Up'), () => + frm.trigger("move_up") + ); frm.add_custom_button(__('Bisect'), () => frm.trigger("bisect") ); - frm.change_custom_button_type(__('Bisect'), null, 'primary'); + // frm.change_custom_button_type(__('Bisect'), null, 'primary'); }, bisect(frm) { frm.call({ From 81f456bdc78272807aa6dadb26d2f5f156de2a24 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 25 Sep 2023 12:16:47 +0530 Subject: [PATCH 65/94] refactor: introduce `node` class (cherry picked from commit d53b34c0ce9f469fec79b0cbc08e4cbc47344b46) --- .../bisect_accounting_statements.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py index e032083f9033..3582033abce7 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py @@ -1,6 +1,7 @@ # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +import datetime from collections import deque from math import floor @@ -11,6 +12,36 @@ from frappe.utils import getdate +class Node(object): + def __init__(self): + self.parent = None + self.left_child = None + self.right_child = None + + self.current_period = None + self.difference = 0.0 + self.profit_and_loss_summary = 0.0 + self.balance_sheet_summary = 0.0 + + def update_parent(self): + pass + + def update_left_child(self): + pass + + def update_right_child(self): + pass + + def make_node( + self, + parent: int = None, + period: (datetime, datetime) = None, + left: int = None, + right: int = None, + ): + current_period = period + + class BisectAccountingStatements(Document): def bfs(self): period_list = deque([(getdate(self.from_date), getdate(self.to_date))]) From 66fae6466f4245a0b9d5a25d42c7d0ba972e77fd Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 25 Sep 2023 20:59:39 +0530 Subject: [PATCH 66/94] refactor: ability to build and load tree from DB (cherry picked from commit b2dde55f2ccf337e2b07aac8b62c24791c4975c9) --- .../bisect_accounting_statements.js | 8 +- .../bisect_accounting_statements.json | 43 +++- .../bisect_accounting_statements.py | 195 +++++++++++++----- 3 files changed, 189 insertions(+), 57 deletions(-) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js index 69bd56e73f29..732b2b0f9cd9 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js @@ -14,15 +14,15 @@ frappe.ui.form.on("Bisect Accounting Statements", { frm.add_custom_button(__('Up'), () => frm.trigger("move_up") ); - frm.add_custom_button(__('Bisect'), () => - frm.trigger("bisect") + frm.add_custom_button(__('Build Tree'), () => + frm.trigger("build_tree") ); // frm.change_custom_button_type(__('Bisect'), null, 'primary'); }, - bisect(frm) { + build_tree(frm) { frm.call({ doc: frm.doc, - method: 'bisect', + method: 'build_tree', callback: (r) => { console.log(r); } diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json index b2f3c4bb90be..e0b84adeeb53 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json @@ -11,7 +11,14 @@ "column_break_qxbi", "to_date", "column_break_iwny", - "algorithm" + "algorithm", + "section_break_lwr2", + "current_from_date", + "column_break_uuic", + "current_to_date", + "section_break_zbty", + "current_node", + "tree" ], "fields": [ { @@ -38,13 +45,45 @@ { "fieldname": "column_break_iwny", "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_zbty", + "fieldtype": "Section Break" + }, + { + "fieldname": "tree", + "fieldtype": "JSON", + "label": "Tree" + }, + { + "fieldname": "current_node", + "fieldtype": "JSON", + "label": "Current Node" + }, + { + "fieldname": "section_break_lwr2", + "fieldtype": "Section Break" + }, + { + "fieldname": "current_from_date", + "fieldtype": "Date", + "label": "Current From Date" + }, + { + "fieldname": "current_to_date", + "fieldtype": "Date", + "label": "Current To Date" + }, + { + "fieldname": "column_break_uuic", + "fieldtype": "Column Break" } ], "hide_toolbar": 1, "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-09-25 10:50:53.887235", + "modified": "2023-09-25 17:05:13.384320", "modified_by": "Administrator", "module": "Accounts", "name": "Bisect Accounting Statements", diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py index 3582033abce7..7c68f18775e8 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py @@ -2,6 +2,7 @@ # For license information, please see license.txt import datetime +import json from collections import deque from math import floor @@ -10,85 +11,162 @@ from frappe import _ from frappe.model.document import Document from frappe.utils import getdate +from frappe.utils.data import DATETIME_FORMAT, guess_date_format class Node(object): - def __init__(self): - self.parent = None - self.left_child = None - self.right_child = None + def __init__( + self, + parent: int = None, + period: (datetime, datetime) = None, + left: int = None, + right: int = None, + ): + self.parent = parent + self.left_child = left + self.right_child = right - self.current_period = None + self.period = period self.difference = 0.0 self.profit_and_loss_summary = 0.0 self.balance_sheet_summary = 0.0 - def update_parent(self): - pass + def as_dict(self): + return dict( + parent=self.parent, + left_child=self.left_child, + right_child=self.right_child, + period=(self.period[0].strftime(DATETIME_FORMAT), self.period[1].strftime(DATETIME_FORMAT)), + difference=self.difference, + profit_and_loss_summary=self.profit_and_loss_summary, + balance_sheet_summary=self.balance_sheet_summary, + ) - def update_left_child(self): - pass + def __repr__(self): + return f"Node (parent: {self.parent}, left_child: {self.left_child}, right_child: {self.right_child}, period: {self.period})" - def update_right_child(self): - pass - def make_node( - self, - parent: int = None, - period: (datetime, datetime) = None, - left: int = None, - right: int = None, - ): - current_period = period +class BTree(object): + def __init__(self): + self.btree = [] + self.current_node = None + def as_list(self): + lst = [] + for x in self.btree: + lst.append(x.as_dict()) + return lst + + def bfs(self, from_date: datetime, to_date: datetime): + root_node = Node(parent=None, period=(getdate(from_date), getdate(to_date))) + root_node.parent = None + + # add root node to tree + self.btree.append(root_node) + cur_node = root_node + period_list = deque([root_node]) -class BisectAccountingStatements(Document): - def bfs(self): - period_list = deque([(getdate(self.from_date), getdate(self.to_date))]) periods = [] - dates = [] while period_list: - cur_frm_date, cur_to_date = period_list.popleft() - delta = cur_to_date - cur_frm_date - periods.append((cur_frm_date, cur_to_date, delta)) + cur_node = period_list.popleft() + cur_node_index = len(self.btree) - 1 + + delta = cur_node.period[1] - cur_node.period[0] if delta.days == 0: continue else: cur_floor = floor(delta.days / 2) - left = (cur_frm_date, (cur_frm_date + relativedelta(days=+cur_floor))) - right = ((cur_frm_date + relativedelta(days=+(cur_floor + 1))), cur_to_date) - period_list.append(left) - period_list.append(right) - return periods - - def dfs(self): - period_list = [(getdate(self.from_date), getdate(self.to_date))] + left = (cur_node.period[0], (cur_node.period[0] + relativedelta(days=+cur_floor))) + left_node = Node(parent=cur_node_index, period=left) + self.btree.append(left_node) + cur_node.left_child = len(self.btree) - 1 + period_list.append(left_node) + + right = ((cur_node.period[0] + relativedelta(days=+(cur_floor + 1))), cur_node.period[1]) + right_node = Node(parent=cur_node_index, period=right) + self.btree.append(right_node) + cur_node.right_child = len(self.btree) - 1 + period_list.append(right_node) + + def dfs(self, from_date: datetime, to_date: datetime): + root_node = Node(parent=None, period=(getdate(from_date), getdate(to_date))) + root_node.parent = None + + # add root node to tree + self.btree.append(root_node) + cur_node = root_node + period_list = [root_node] + periods = [] while period_list: - cur_frm_date, cur_to_date = period_list.pop() - delta = cur_to_date - cur_frm_date - periods.append((cur_frm_date, cur_to_date, delta)) + cur_node = period_list.pop() + cur_node_index = len(self.btree) - 1 + + delta = cur_node.period[1] - cur_node.period[0] if delta.days == 0: continue else: cur_floor = floor(delta.days / 2) - left = (cur_frm_date, (cur_frm_date + relativedelta(days=+cur_floor))) - right = ((cur_frm_date + relativedelta(days=+(cur_floor + 1))), cur_to_date) - period_list.append(left) - period_list.append(right) - return periods + left = (cur_node.period[0], (cur_node.period[0] + relativedelta(days=+cur_floor))) + left_node = Node(parent=cur_node_index, period=left) + self.btree.append(left_node) + cur_node.left_child = len(self.btree) - 1 + period_list.append(left_node) + + right = ((cur_node.period[0] + relativedelta(days=+(cur_floor + 1))), cur_node.period[1]) + right_node = Node(parent=cur_node_index, period=right) + self.btree.append(right_node) + cur_node.right_child = len(self.btree) - 1 + period_list.append(right_node) + + def load_tree(self, tree: list, current_node: dict): + self.btree = [] + tree = json.loads(tree) + for x in tree: + x = frappe._dict(x) + n = Node(x.parent, x.period, x.left_child, x.right_child) + n.period = x.period + n.difference = x.difference + x.profit_and_loss_summary = x.profit_and_loss_summary + x.balance_sheet_summary = x.balance_sheet_summary + self.btree.append(n) + + current_node = frappe._dict(json.loads(current_node)) + n = Node( + current_node.parent, current_node.period, current_node.left_child, current_node.right_child + ) + n.period = current_node.period + n.difference = current_node.difference + n.profit_and_loss_summary = current_node.profit_and_loss_summary + n.balance_sheet_summary = current_node.balance_sheet_summary + self.current_node = n + + def build_tree(self, from_date: datetime, to_date: datetime, alogrithm: str): + if alogrithm == "BFS": + self.bfs(from_date, to_date) + + if alogrithm == "DFS": + self.dfs(from_date, to_date) + + # set root as current node + self.current_node = self.btree[0] + + def bisec_left(self): + pass - @frappe.whitelist() - def bisect(self): - if self.algorithm == "BFS": - periods = self.bfs() + def bisect_right(self): + pass + + def move_up(self): + pass - if self.algorithm == "DFS": - periods = self.dfs() - print("Periods: ", len(periods)) - for x in periods: - print(x) +class BisectAccountingStatements(Document): + def __init__(self, *args, **kwargs): + super(BisectAccountingStatements, self).__init__(*args, **kwargs) + if self.tree and self.current_node: + self.tree_instance = BTree() + self.tree_instance.load_tree(self.tree, self.current_node) def validate(self): self.validate_dates() @@ -100,3 +178,18 @@ def validate_dates(self): frappe.bold(self.from_date), frappe.bold(self.to_date) ) ) + + @frappe.whitelist() + def build_tree(self): + self.tree_instance = BTree() + self.tree_instance.build_tree(self.from_date, self.to_date, self.algorithm) + print("printing tree") + for x in self.tree_instance.btree: + print(x) + + print("Root", self.tree_instnace.current_node) + + self.tree = json.dumps(self.tree_instance.as_list()) + self.current_node = json.dumps(self.tree_intance.btree[0].as_dict()) + + print(guess_date_format(json.loads(self.current_node)["period"][0])) From 674d82298567fc892911747de44a4a1f9fac5dc7 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 25 Sep 2023 21:16:18 +0530 Subject: [PATCH 67/94] refactor: add basic navigation 1. remove unnecessary columns 2. added basic tree navigation (cherry picked from commit 705ef4f5a31aac60ac879adecbede33ffd573588) --- .../bisect_accounting_statements.js | 27 ++++++++++++ .../bisect_accounting_statements.json | 24 +---------- .../bisect_accounting_statements.py | 42 ++++++++++++++++--- 3 files changed, 65 insertions(+), 28 deletions(-) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js index 732b2b0f9cd9..4e478ee4ab51 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js @@ -19,6 +19,33 @@ frappe.ui.form.on("Bisect Accounting Statements", { ); // frm.change_custom_button_type(__('Bisect'), null, 'primary'); }, + bisect_left(frm) { + frm.call({ + doc: frm.doc, + method: 'bisect_left', + callback: (r) => { + console.log(r); + } + }); + }, + bisect_right(frm) { + frm.call({ + doc: frm.doc, + method: 'bisect_right', + callback: (r) => { + console.log(r); + } + }); + }, + move_up(frm) { + frm.call({ + doc: frm.doc, + method: 'move_up', + callback: (r) => { + console.log(r); + } + }); + }, build_tree(frm) { frm.call({ doc: frm.doc, diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json index e0b84adeeb53..c5715fe2ea73 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json @@ -12,10 +12,6 @@ "to_date", "column_break_iwny", "algorithm", - "section_break_lwr2", - "current_from_date", - "column_break_uuic", - "current_to_date", "section_break_zbty", "current_node", "tree" @@ -59,31 +55,13 @@ "fieldname": "current_node", "fieldtype": "JSON", "label": "Current Node" - }, - { - "fieldname": "section_break_lwr2", - "fieldtype": "Section Break" - }, - { - "fieldname": "current_from_date", - "fieldtype": "Date", - "label": "Current From Date" - }, - { - "fieldname": "current_to_date", - "fieldtype": "Date", - "label": "Current To Date" - }, - { - "fieldname": "column_break_uuic", - "fieldtype": "Column Break" } ], "hide_toolbar": 1, "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-09-25 17:05:13.384320", + "modified": "2023-09-25 21:15:47.905386", "modified_by": "Administrator", "module": "Accounts", "name": "Bisect Accounting Statements", diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py index 7c68f18775e8..accc8a9b42bd 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py @@ -124,8 +124,12 @@ def load_tree(self, tree: list, current_node: dict): tree = json.loads(tree) for x in tree: x = frappe._dict(x) - n = Node(x.parent, x.period, x.left_child, x.right_child) - n.period = x.period + n = Node(x.parent, None, x.left_child, x.right_child) + date_format = guess_date_format(x.period[0]) + n.period = ( + datetime.datetime.strptime(x.period[0], date_format), + datetime.datetime.strptime(x.period[1], date_format), + ) n.difference = x.difference x.profit_and_loss_summary = x.profit_and_loss_summary x.balance_sheet_summary = x.balance_sheet_summary @@ -187,9 +191,37 @@ def build_tree(self): for x in self.tree_instance.btree: print(x) - print("Root", self.tree_instnace.current_node) + print("Root", self.tree_instance.current_node) self.tree = json.dumps(self.tree_instance.as_list()) - self.current_node = json.dumps(self.tree_intance.btree[0].as_dict()) + self.current_node = json.dumps(self.tree_instance.btree[0].as_dict()) + + @frappe.whitelist() + def bisect_left(self): + if self.tree_instance.current_node is not None: + if self.tree_instance.current_node.left_child is not None: + self.current_node = self.tree_instance.btree[self.tree_instance.current_node.left_child] + self.current_node = json.dumps(self.current_node.as_dict()) + self.save() + else: + frappe.msgprint("No more children on Left") - print(guess_date_format(json.loads(self.current_node)["period"][0])) + @frappe.whitelist() + def bisect_right(self): + if self.tree_instance.current_node is not None: + if self.tree_instance.current_node.right_child is not None: + self.current_node = self.tree_instance.btree[self.tree_instance.current_node.right_child] + self.current_node = json.dumps(self.current_node.as_dict()) + self.save() + else: + frappe.msgprint("No more children on Right") + + @frappe.whitelist() + def move_up(self): + if self.tree_instance.current_node is not None: + if self.tree_instance.current_node.parent is not None: + self.current_node = self.tree_instance.btree[self.tree_instance.current_node.parent] + self.current_node = json.dumps(self.current_node.as_dict()) + self.save() + else: + frappe.msgprint("Reached Root") From 609ecbe804845407e344136cb273c52301f10cfe Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 25 Sep 2023 22:01:07 +0530 Subject: [PATCH 68/94] chore: remove unwanted code (cherry picked from commit de2eba0d98409083c43affb485f4a44241e79b87) --- .../bisect_accounting_statements.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py index accc8a9b42bd..950d5afab311 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py @@ -66,7 +66,6 @@ def bfs(self, from_date: datetime, to_date: datetime): cur_node = root_node period_list = deque([root_node]) - periods = [] while period_list: cur_node = period_list.popleft() cur_node_index = len(self.btree) - 1 @@ -97,7 +96,6 @@ def dfs(self, from_date: datetime, to_date: datetime): cur_node = root_node period_list = [root_node] - periods = [] while period_list: cur_node = period_list.pop() cur_node_index = len(self.btree) - 1 From 6afb6ff4dff51c861901c828061a11fb40ae4364 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 25 Sep 2023 22:06:05 +0530 Subject: [PATCH 69/94] feat: nodes doctype (cherry picked from commit 85f2a6dd545a6e55d3c4d7fe64bea39a3122598c) --- erpnext/accounts/doctype/nodes/__init__.py | 0 erpnext/accounts/doctype/nodes/nodes.js | 8 ++ erpnext/accounts/doctype/nodes/nodes.json | 89 ++++++++++++++++++++ erpnext/accounts/doctype/nodes/nodes.py | 9 ++ erpnext/accounts/doctype/nodes/test_nodes.py | 9 ++ 5 files changed, 115 insertions(+) create mode 100644 erpnext/accounts/doctype/nodes/__init__.py create mode 100644 erpnext/accounts/doctype/nodes/nodes.js create mode 100644 erpnext/accounts/doctype/nodes/nodes.json create mode 100644 erpnext/accounts/doctype/nodes/nodes.py create mode 100644 erpnext/accounts/doctype/nodes/test_nodes.py diff --git a/erpnext/accounts/doctype/nodes/__init__.py b/erpnext/accounts/doctype/nodes/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/accounts/doctype/nodes/nodes.js b/erpnext/accounts/doctype/nodes/nodes.js new file mode 100644 index 000000000000..bd74d68637aa --- /dev/null +++ b/erpnext/accounts/doctype/nodes/nodes.js @@ -0,0 +1,8 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Nodes", { +// refresh(frm) { + +// }, +// }); diff --git a/erpnext/accounts/doctype/nodes/nodes.json b/erpnext/accounts/doctype/nodes/nodes.json new file mode 100644 index 000000000000..1238c8136afe --- /dev/null +++ b/erpnext/accounts/doctype/nodes/nodes.json @@ -0,0 +1,89 @@ +{ + "actions": [], + "autoname": "autoincrement", + "creation": "2023-09-25 22:01:33.961832", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "root", + "left_child", + "right_child", + "period_from_date", + "period_to_date", + "difference", + "balance_sheet_summary", + "profit_loss_summary" + ], + "fields": [ + { + "fieldname": "root", + "fieldtype": "Link", + "label": "Root", + "options": "Nodes" + }, + { + "fieldname": "left_child", + "fieldtype": "Link", + "label": "Left Child", + "options": "Nodes" + }, + { + "fieldname": "right_child", + "fieldtype": "Link", + "label": "Right Child", + "options": "Nodes" + }, + { + "fieldname": "period_from_date", + "fieldtype": "Datetime", + "label": "Period_from_date" + }, + { + "fieldname": "period_to_date", + "fieldtype": "Datetime", + "label": "Period To Date" + }, + { + "fieldname": "difference", + "fieldtype": "Float", + "label": "Difference" + }, + { + "fieldname": "balance_sheet_summary", + "fieldtype": "Float", + "label": "Balance Sheet Summary" + }, + { + "fieldname": "profit_loss_summary", + "fieldtype": "Float", + "label": "Profit and Loss Summary" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2023-09-25 22:05:49.577861", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Nodes", + "naming_rule": "Autoincrement", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/nodes/nodes.py b/erpnext/accounts/doctype/nodes/nodes.py new file mode 100644 index 000000000000..67d5d69bfe7d --- /dev/null +++ b/erpnext/accounts/doctype/nodes/nodes.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class Nodes(Document): + pass diff --git a/erpnext/accounts/doctype/nodes/test_nodes.py b/erpnext/accounts/doctype/nodes/test_nodes.py new file mode 100644 index 000000000000..feeef765e034 --- /dev/null +++ b/erpnext/accounts/doctype/nodes/test_nodes.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestNodes(FrappeTestCase): + pass From bc1f25b89799cc7c34b950103f66b2d6857cdb16 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 26 Sep 2023 08:43:59 +0530 Subject: [PATCH 70/94] chore: use doctype as btree (cherry picked from commit 9d2025636689cf0c6f44f9784537d9eb132b820c) --- .../bisect_accounting_statements.py | 51 +++++++++++++------ 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py index 950d5afab311..53319b3c58c1 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py @@ -58,33 +58,51 @@ def as_list(self): return lst def bfs(self, from_date: datetime, to_date: datetime): - root_node = Node(parent=None, period=(getdate(from_date), getdate(to_date))) - root_node.parent = None + node = frappe.new_doc("Nodes") + node.period_from_date = from_date + node.period_to_date = to_date + node.root = None + node.insert() - # add root node to tree - self.btree.append(root_node) - cur_node = root_node - period_list = deque([root_node]) + period_list = deque([node]) while period_list: cur_node = period_list.popleft() - cur_node_index = len(self.btree) - 1 - delta = cur_node.period[1] - cur_node.period[0] + print(cur_node.as_dict()) + delta = cur_node.period_to_date - cur_node.period_from_date if delta.days == 0: continue else: cur_floor = floor(delta.days / 2) - left = (cur_node.period[0], (cur_node.period[0] + relativedelta(days=+cur_floor))) - left_node = Node(parent=cur_node_index, period=left) - self.btree.append(left_node) - cur_node.left_child = len(self.btree) - 1 + left = ( + cur_node.period_from_date, + (cur_node.period_from_date + relativedelta(days=+cur_floor)), + ) + left_node = frappe.get_doc( + { + "doctype": "Nodes", + "period_from_date": cur_node.period_from_date, + "period_to_date": left, + "root": cur_node.name, + } + ).insert() + cur_node.left_child = left_node.name period_list.append(left_node) - right = ((cur_node.period[0] + relativedelta(days=+(cur_floor + 1))), cur_node.period[1]) - right_node = Node(parent=cur_node_index, period=right) - self.btree.append(right_node) - cur_node.right_child = len(self.btree) - 1 + right = ( + (cur_node.period_from_date + relativedelta(days=+(cur_floor + 1))), + cur_node.period_to_date, + ) + right_node = frappe.get_doc( + { + "doctype": "Nodes", + "period_from_date": right, + "period_to_date": cur_node.period_to_date, + "root": cur_node.name, + } + ).insert() + cur_node.right_child = right_node period_list.append(right_node) def dfs(self, from_date: datetime, to_date: datetime): @@ -144,6 +162,7 @@ def load_tree(self, tree: list, current_node: dict): self.current_node = n def build_tree(self, from_date: datetime, to_date: datetime, alogrithm: str): + frappe.db.delete("Nodes") if alogrithm == "BFS": self.bfs(from_date, to_date) From bee574430969ec5082736fab3b13ed597a462f6b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 26 Sep 2023 12:28:38 +0530 Subject: [PATCH 71/94] refactor: use DB to store tree and state (cherry picked from commit 99fbd8ad186c989faa86863d5902a3b4d2015505) --- .../bisect_accounting_statements.json | 19 +- .../bisect_accounting_statements.py | 284 ++++++------------ 2 files changed, 105 insertions(+), 198 deletions(-) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json index c5715fe2ea73..0de820a80f14 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json @@ -13,8 +13,7 @@ "column_break_iwny", "algorithm", "section_break_zbty", - "current_node", - "tree" + "current_node" ], "fields": [ { @@ -23,12 +22,12 @@ }, { "fieldname": "from_date", - "fieldtype": "Date", + "fieldtype": "Datetime", "label": "From Date" }, { "fieldname": "to_date", - "fieldtype": "Date", + "fieldtype": "Datetime", "label": "To Date" }, { @@ -46,22 +45,18 @@ "fieldname": "section_break_zbty", "fieldtype": "Section Break" }, - { - "fieldname": "tree", - "fieldtype": "JSON", - "label": "Tree" - }, { "fieldname": "current_node", - "fieldtype": "JSON", - "label": "Current Node" + "fieldtype": "Link", + "label": "Current Node", + "options": "Nodes" } ], "hide_toolbar": 1, "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-09-25 21:15:47.905386", + "modified": "2023-09-26 12:09:23.649156", "modified_by": "Administrator", "module": "Accounts", "name": "Bisect Accounting Statements", diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py index 53319b3c58c1..8730772e29d4 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py @@ -2,7 +2,6 @@ # For license information, please see license.txt import datetime -import json from collections import deque from math import floor @@ -11,234 +10,147 @@ from frappe import _ from frappe.model.document import Document from frappe.utils import getdate -from frappe.utils.data import DATETIME_FORMAT, guess_date_format - - -class Node(object): - def __init__( - self, - parent: int = None, - period: (datetime, datetime) = None, - left: int = None, - right: int = None, - ): - self.parent = parent - self.left_child = left - self.right_child = right - - self.period = period - self.difference = 0.0 - self.profit_and_loss_summary = 0.0 - self.balance_sheet_summary = 0.0 - - def as_dict(self): - return dict( - parent=self.parent, - left_child=self.left_child, - right_child=self.right_child, - period=(self.period[0].strftime(DATETIME_FORMAT), self.period[1].strftime(DATETIME_FORMAT)), - difference=self.difference, - profit_and_loss_summary=self.profit_and_loss_summary, - balance_sheet_summary=self.balance_sheet_summary, - ) - - def __repr__(self): - return f"Node (parent: {self.parent}, left_child: {self.left_child}, right_child: {self.right_child}, period: {self.period})" - - -class BTree(object): - def __init__(self): - self.btree = [] - self.current_node = None - - def as_list(self): - lst = [] - for x in self.btree: - lst.append(x.as_dict()) - return lst +from frappe.utils.data import guess_date_format + + +class BisectAccountingStatements(Document): + def validate(self): + self.validate_dates() + + def validate_dates(self): + if getdate(self.from_date) > getdate(self.to_date): + frappe.throw( + _("From Date: {0} cannot be greater than To date: {1}").format( + frappe.bold(self.from_date), frappe.bold(self.to_date) + ) + ) def bfs(self, from_date: datetime, to_date: datetime): + # Make Root node node = frappe.new_doc("Nodes") + node.root = None node.period_from_date = from_date node.period_to_date = to_date - node.root = None node.insert() - period_list = deque([node]) - - while period_list: - cur_node = period_list.popleft() - - print(cur_node.as_dict()) + period_queue = deque([node]) + while period_queue: + cur_node = period_queue.popleft() delta = cur_node.period_to_date - cur_node.period_from_date if delta.days == 0: continue else: cur_floor = floor(delta.days / 2) - left = ( - cur_node.period_from_date, - (cur_node.period_from_date + relativedelta(days=+cur_floor)), - ) - left_node = frappe.get_doc( - { - "doctype": "Nodes", - "period_from_date": cur_node.period_from_date, - "period_to_date": left, - "root": cur_node.name, - } - ).insert() + next_to_date = cur_node.period_from_date + relativedelta(days=+cur_floor) + left_node = frappe.new_doc("Nodes") + left_node.period_from_date = cur_node.period_from_date + left_node.period_to_date = next_to_date + left_node.root = cur_node.name + left_node.insert() cur_node.left_child = left_node.name - period_list.append(left_node) - - right = ( - (cur_node.period_from_date + relativedelta(days=+(cur_floor + 1))), - cur_node.period_to_date, - ) - right_node = frappe.get_doc( - { - "doctype": "Nodes", - "period_from_date": right, - "period_to_date": cur_node.period_to_date, - "root": cur_node.name, - } - ).insert() - cur_node.right_child = right_node - period_list.append(right_node) + period_queue.append(left_node) - def dfs(self, from_date: datetime, to_date: datetime): - root_node = Node(parent=None, period=(getdate(from_date), getdate(to_date))) - root_node.parent = None + next_from_date = cur_node.period_from_date + relativedelta(days=+(cur_floor + 1)) + right_node = frappe.new_doc("Nodes") + right_node.period_from_date = next_from_date + right_node.period_to_date = cur_node.period_to_date + right_node.root = cur_node.name + right_node.insert() + cur_node.right_child = right_node.name + period_queue.append(right_node) - # add root node to tree - self.btree.append(root_node) - cur_node = root_node - period_list = [root_node] + cur_node.save() - while period_list: - cur_node = period_list.pop() - cur_node_index = len(self.btree) - 1 + def dfs(self, from_date: datetime, to_date: datetime): + # Make Root node + node = frappe.new_doc("Nodes") + node.root = None + node.period_from_date = from_date + node.period_to_date = to_date + node.insert() - delta = cur_node.period[1] - cur_node.period[0] + period_stack = [node] + while period_stack: + cur_node = period_stack.pop() + delta = cur_node.period_to_date - cur_node.period_from_date if delta.days == 0: continue else: cur_floor = floor(delta.days / 2) - left = (cur_node.period[0], (cur_node.period[0] + relativedelta(days=+cur_floor))) - left_node = Node(parent=cur_node_index, period=left) - self.btree.append(left_node) - cur_node.left_child = len(self.btree) - 1 - period_list.append(left_node) - - right = ((cur_node.period[0] + relativedelta(days=+(cur_floor + 1))), cur_node.period[1]) - right_node = Node(parent=cur_node_index, period=right) - self.btree.append(right_node) - cur_node.right_child = len(self.btree) - 1 - period_list.append(right_node) - - def load_tree(self, tree: list, current_node: dict): - self.btree = [] - tree = json.loads(tree) - for x in tree: - x = frappe._dict(x) - n = Node(x.parent, None, x.left_child, x.right_child) - date_format = guess_date_format(x.period[0]) - n.period = ( - datetime.datetime.strptime(x.period[0], date_format), - datetime.datetime.strptime(x.period[1], date_format), - ) - n.difference = x.difference - x.profit_and_loss_summary = x.profit_and_loss_summary - x.balance_sheet_summary = x.balance_sheet_summary - self.btree.append(n) - - current_node = frappe._dict(json.loads(current_node)) - n = Node( - current_node.parent, current_node.period, current_node.left_child, current_node.right_child - ) - n.period = current_node.period - n.difference = current_node.difference - n.profit_and_loss_summary = current_node.profit_and_loss_summary - n.balance_sheet_summary = current_node.balance_sheet_summary - self.current_node = n - - def build_tree(self, from_date: datetime, to_date: datetime, alogrithm: str): - frappe.db.delete("Nodes") - if alogrithm == "BFS": - self.bfs(from_date, to_date) - - if alogrithm == "DFS": - self.dfs(from_date, to_date) - - # set root as current node - self.current_node = self.btree[0] - - def bisec_left(self): - pass - - def bisect_right(self): - pass - - def move_up(self): - pass + next_to_date = cur_node.period_from_date + relativedelta(days=+cur_floor) + left_node = frappe.new_doc("Nodes") + left_node.period_from_date = cur_node.period_from_date + left_node.period_to_date = next_to_date + left_node.root = cur_node.name + left_node.insert() + cur_node.left_child = left_node.name + period_stack.append(left_node) + next_from_date = cur_node.period_from_date + relativedelta(days=+(cur_floor + 1)) + right_node = frappe.new_doc("Nodes") + right_node.period_from_date = next_from_date + right_node.period_to_date = cur_node.period_to_date + right_node.root = cur_node.name + right_node.insert() + cur_node.right_child = right_node.name + period_stack.append(right_node) -class BisectAccountingStatements(Document): - def __init__(self, *args, **kwargs): - super(BisectAccountingStatements, self).__init__(*args, **kwargs) - if self.tree and self.current_node: - self.tree_instance = BTree() - self.tree_instance.load_tree(self.tree, self.current_node) - - def validate(self): - self.validate_dates() - - def validate_dates(self): - if getdate(self.from_date) > getdate(self.to_date): - frappe.throw( - _("From Date: {0} cannot be greater than To date: {1}").format( - frappe.bold(self.from_date), frappe.bold(self.to_date) - ) - ) + cur_node.save() @frappe.whitelist() def build_tree(self): - self.tree_instance = BTree() - self.tree_instance.build_tree(self.from_date, self.to_date, self.algorithm) - print("printing tree") - for x in self.tree_instance.btree: - print(x) + frappe.db.delete("Nodes") - print("Root", self.tree_instance.current_node) + # Convert str to datetime format + dt_format = guess_date_format(self.from_date) + from_date = datetime.datetime.strptime(self.from_date, dt_format) + to_date = datetime.datetime.strptime(self.to_date, dt_format) + + if self.algorithm == "BFS": + self.bfs(from_date, to_date) - self.tree = json.dumps(self.tree_instance.as_list()) - self.current_node = json.dumps(self.tree_instance.btree[0].as_dict()) + if self.algorithm == "DFS": + self.dfs(from_date, to_date) + + # set root as current node + root = frappe.db.get_all("Nodes", filters={"root": ["is", "not set"]})[0] + frappe.db.set_single_value("Bisect Accounting Statements", "current_node", root.name) @frappe.whitelist() def bisect_left(self): - if self.tree_instance.current_node is not None: - if self.tree_instance.current_node.left_child is not None: - self.current_node = self.tree_instance.btree[self.tree_instance.current_node.left_child] - self.current_node = json.dumps(self.current_node.as_dict()) + if self.current_node is not None: + cur_node = frappe.get_doc("Nodes", self.current_node) + if cur_node.left_child is not None: + lft_node = frappe.get_doc("Nodes", cur_node.left_child) + self.current_node = cur_node.left_child + self.from_date = lft_node.period_from_date + self.to_date = lft_node.period_to_date self.save() else: frappe.msgprint("No more children on Left") @frappe.whitelist() def bisect_right(self): - if self.tree_instance.current_node is not None: - if self.tree_instance.current_node.right_child is not None: - self.current_node = self.tree_instance.btree[self.tree_instance.current_node.right_child] - self.current_node = json.dumps(self.current_node.as_dict()) + if self.current_node is not None: + cur_node = frappe.get_doc("Nodes", self.current_node) + if cur_node.right_child is not None: + rgt_node = frappe.get_doc("Nodes", cur_node.right_child) + self.current_node = cur_node.right_child + self.from_date = rgt_node.period_from_date + self.to_date = rgt_node.period_to_date self.save() else: frappe.msgprint("No more children on Right") @frappe.whitelist() def move_up(self): - if self.tree_instance.current_node is not None: - if self.tree_instance.current_node.parent is not None: - self.current_node = self.tree_instance.btree[self.tree_instance.current_node.parent] - self.current_node = json.dumps(self.current_node.as_dict()) + if self.current_node is not None: + cur_node = frappe.get_doc("Nodes", self.current_node) + if cur_node.root is not None: + root = frappe.get_doc("Nodes", cur_node.root) + self.current_node = cur_node.root + self.from_date = root.period_from_date + self.to_date = root.period_to_date self.save() else: frappe.msgprint("Reached Root") From f06014afc3aac9e96e13cb19053c0559fb9397c1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 26 Sep 2023 20:33:13 +0530 Subject: [PATCH 72/94] refactor: calculate summary on tree navigation (cherry picked from commit f7b7b2b438c3c5496097de1a740d6d4ede09605b) --- .../bisect_accounting_statements.js | 3 +- .../bisect_accounting_statements.json | 77 ++++++++++++++++++- .../bisect_accounting_statements.py | 29 +++++-- 3 files changed, 99 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js index 4e478ee4ab51..7abe4f87aefa 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js @@ -17,7 +17,6 @@ frappe.ui.form.on("Bisect Accounting Statements", { frm.add_custom_button(__('Build Tree'), () => frm.trigger("build_tree") ); - // frm.change_custom_button_type(__('Bisect'), null, 'primary'); }, bisect_left(frm) { frm.call({ @@ -54,5 +53,5 @@ frappe.ui.form.on("Bisect Accounting Statements", { console.log(r); } }); - } + }, }); diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json index 0de820a80f14..c76ef4d06f30 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json @@ -7,13 +7,26 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "company", + "column_break_hcam", "from_date", "column_break_qxbi", "to_date", "column_break_iwny", "algorithm", "section_break_zbty", - "current_node" + "current_node", + "sandbox", + "section_break_hmsy", + "current_from_date", + "column_break_uqyd", + "current_to_date", + "section_break_hbyo", + "p_l_summary", + "column_break_aivo", + "b_s_summary", + "column_break_gvwx", + "difference" ], "fields": [ { @@ -50,13 +63,73 @@ "fieldtype": "Link", "label": "Current Node", "options": "Nodes" + }, + { + "fieldname": "sandbox", + "fieldtype": "HTML", + "label": "sandbox" + }, + { + "fieldname": "section_break_hmsy", + "fieldtype": "Section Break" + }, + { + "fieldname": "current_from_date", + "fieldtype": "Datetime", + "label": "Current From Date" + }, + { + "fieldname": "current_to_date", + "fieldtype": "Datetime", + "label": "Current To Date" + }, + { + "fieldname": "column_break_uqyd", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_hbyo", + "fieldtype": "Section Break" + }, + { + "fieldname": "p_l_summary", + "fieldtype": "Data", + "label": "P&L Summary" + }, + { + "fieldname": "b_s_summary", + "fieldtype": "Data", + "label": "Balance Sheet Summary" + }, + { + "fieldname": "difference", + "fieldtype": "Data", + "label": "Difference" + }, + { + "fieldname": "column_break_aivo", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_gvwx", + "fieldtype": "Column Break" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "fieldname": "column_break_hcam", + "fieldtype": "Column Break" } ], "hide_toolbar": 1, "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-09-26 12:09:23.649156", + "modified": "2023-09-26 21:07:14.290963", "modified_by": "Administrator", "module": "Accounts", "name": "Bisect Accounting Statements", diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py index 8730772e29d4..d2b60a422519 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py @@ -116,6 +116,20 @@ def build_tree(self): root = frappe.db.get_all("Nodes", filters={"root": ["is", "not set"]})[0] frappe.db.set_single_value("Bisect Accounting Statements", "current_node", root.name) + def get_report_summary(self): + filters = { + "company": self.company, + "filter_based_on": "Date Range", + "period_start_date": self.current_from_date, + "period_end_date": self.current_to_date, + "periodicity": "Yearly", + } + pl_summary = frappe.get_doc("Report", "Profit and Loss Statement") + self.p_l_summary = pl_summary.execute_script_report(filters=filters)[5] + bs_summary = frappe.get_doc("Report", "Balance Sheet") + self.b_s_summary = bs_summary.execute_script_report(filters=filters)[5] + self.difference = abs(self.p_l_summary - self.b_s_summary) + @frappe.whitelist() def bisect_left(self): if self.current_node is not None: @@ -123,8 +137,9 @@ def bisect_left(self): if cur_node.left_child is not None: lft_node = frappe.get_doc("Nodes", cur_node.left_child) self.current_node = cur_node.left_child - self.from_date = lft_node.period_from_date - self.to_date = lft_node.period_to_date + self.current_from_date = lft_node.period_from_date + self.current_to_date = lft_node.period_to_date + self.get_report_summary() self.save() else: frappe.msgprint("No more children on Left") @@ -136,8 +151,9 @@ def bisect_right(self): if cur_node.right_child is not None: rgt_node = frappe.get_doc("Nodes", cur_node.right_child) self.current_node = cur_node.right_child - self.from_date = rgt_node.period_from_date - self.to_date = rgt_node.period_to_date + self.current_from_date = rgt_node.period_from_date + self.current_to_date = rgt_node.period_to_date + self.get_report_summary() self.save() else: frappe.msgprint("No more children on Right") @@ -149,8 +165,9 @@ def move_up(self): if cur_node.root is not None: root = frappe.get_doc("Nodes", cur_node.root) self.current_node = cur_node.root - self.from_date = root.period_from_date - self.to_date = root.period_to_date + self.current_from_date = root.period_from_date + self.current_to_date = root.period_to_date + self.get_report_summary() self.save() else: frappe.msgprint("Reached Root") From 3cfc778bdb5fef5b3abf490ee855be4ceb8dcb08 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 26 Sep 2023 21:29:54 +0530 Subject: [PATCH 73/94] chore: hide some internal fields (cherry picked from commit bd3dc6482e9621132a64fecc7e2f203b64812068) --- .../bisect_accounting_statements.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json index c76ef4d06f30..d413f70c15e4 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json @@ -61,12 +61,14 @@ { "fieldname": "current_node", "fieldtype": "Link", + "hidden": 1, "label": "Current Node", "options": "Nodes" }, { "fieldname": "sandbox", "fieldtype": "HTML", + "hidden": 1, "label": "sandbox" }, { @@ -129,7 +131,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-09-26 21:07:14.290963", + "modified": "2023-09-26 21:25:48.779453", "modified_by": "Administrator", "module": "Accounts", "name": "Bisect Accounting Statements", From 9d5c0100716c6885ae2757355a2c6aa24f3ffa88 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 27 Sep 2023 09:35:25 +0530 Subject: [PATCH 74/94] refactor: working heatmap (cherry picked from commit 16db6c2f47ec7b803c765d9e04c5f50c4bd6b95f) --- .../bisect_accounting_statements.js | 58 +++++++++++++++---- .../bisect_accounting_statements.json | 30 +++++----- 2 files changed, 63 insertions(+), 25 deletions(-) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js index 7abe4f87aefa..1c1e3cc6ca57 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js @@ -2,27 +2,58 @@ // For license information, please see license.txt frappe.ui.form.on("Bisect Accounting Statements", { + onload(frm) { + frm.trigger("render_heatmap"); + }, refresh(frm) { - frm.add_custom_button(__('Bisect Left'), () => - frm.trigger("bisect_left") - ); + frm.add_custom_button(__('Bisect Left'), () => { + frm.trigger("bisect_left"); + }); - frm.add_custom_button(__('Bisect Right'), () => - frm.trigger("bisect_right") - ); + frm.add_custom_button(__('Bisect Right'), () => { + frm.trigger("bisect_right"); + }); - frm.add_custom_button(__('Up'), () => - frm.trigger("move_up") - ); - frm.add_custom_button(__('Build Tree'), () => - frm.trigger("build_tree") - ); + frm.add_custom_button(__('Up'), () => { + frm.trigger("move_up"); + }); + frm.add_custom_button(__('Build Tree'), () => { + frm.trigger("build_tree"); + }); + }, + render_heatmap(frm) { + let bisect_heatmap = frm.get_field("bisect_heatmap").$wrapper; + bisect_heatmap.addClass("bisect_heatmap_location"); + + // milliseconds in a day + let msiad=24*60*60*1000; + let datapoints = {}; + let fr_dt = new Date(frm.doc.current_from_date).getTime(); + let to_dt = new Date(frm.doc.current_to_date).getTime(); + for(let x=fr_dt; x <= to_dt; x+=msiad){ + let epoch_in_seconds = x/1000; + datapoints[epoch_in_seconds] = 1; + } + console.log(datapoints); + + new frappe.Chart(".bisect_heatmap_location", { + type: "heatmap", + title: "Bisecting On", + data: { + dataPoints: datapoints, + start: new Date(frm.doc.from_date), + end: new Date(frm.doc.to_date), + }, + countLabel: 'Difference', + discreteDomains: 1 + }); }, bisect_left(frm) { frm.call({ doc: frm.doc, method: 'bisect_left', callback: (r) => { + frm.trigger("render_heatmap"); console.log(r); } }); @@ -32,6 +63,7 @@ frappe.ui.form.on("Bisect Accounting Statements", { doc: frm.doc, method: 'bisect_right', callback: (r) => { + frm.trigger("render_heatmap"); console.log(r); } }); @@ -41,6 +73,7 @@ frappe.ui.form.on("Bisect Accounting Statements", { doc: frm.doc, method: 'move_up', callback: (r) => { + frm.trigger("render_heatmap"); console.log(r); } }); @@ -50,6 +83,7 @@ frappe.ui.form.on("Bisect Accounting Statements", { doc: frm.doc, method: 'build_tree', callback: (r) => { + frm.trigger("render_heatmap"); console.log(r); } }); diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json index d413f70c15e4..9a7242fe922a 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json @@ -14,13 +14,14 @@ "to_date", "column_break_iwny", "algorithm", - "section_break_zbty", + "section_break_8ph9", "current_node", - "sandbox", "section_break_hmsy", "current_from_date", "column_break_uqyd", "current_to_date", + "section_break_ngid", + "bisect_heatmap", "section_break_hbyo", "p_l_summary", "column_break_aivo", @@ -54,10 +55,6 @@ "fieldname": "column_break_iwny", "fieldtype": "Column Break" }, - { - "fieldname": "section_break_zbty", - "fieldtype": "Section Break" - }, { "fieldname": "current_node", "fieldtype": "Link", @@ -65,12 +62,6 @@ "label": "Current Node", "options": "Nodes" }, - { - "fieldname": "sandbox", - "fieldtype": "HTML", - "hidden": 1, - "label": "sandbox" - }, { "fieldname": "section_break_hmsy", "fieldtype": "Section Break" @@ -125,13 +116,26 @@ { "fieldname": "column_break_hcam", "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_ngid", + "fieldtype": "Section Break" + }, + { + "fieldname": "section_break_8ph9", + "fieldtype": "Section Break" + }, + { + "fieldname": "bisect_heatmap", + "fieldtype": "HTML", + "label": "Heatmap" } ], "hide_toolbar": 1, "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-09-26 21:25:48.779453", + "modified": "2023-09-27 12:10:40.044935", "modified_by": "Administrator", "module": "Accounts", "name": "Bisect Accounting Statements", From c2f6f9d37f514b41b59a384c8384577112ff60c9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 27 Sep 2023 14:40:57 +0530 Subject: [PATCH 75/94] chore: hide internal fields and better painting logic for heatmap (cherry picked from commit f6831fba1370e569299c63f592fcd2a09a1dbc76) --- .../bisect_accounting_statements.js | 15 ++++++++---- .../bisect_accounting_statements.json | 24 ++++++++++++------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js index 1c1e3cc6ca57..c00e49d3805f 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js @@ -28,11 +28,18 @@ frappe.ui.form.on("Bisect Accounting Statements", { // milliseconds in a day let msiad=24*60*60*1000; let datapoints = {}; - let fr_dt = new Date(frm.doc.current_from_date).getTime(); - let to_dt = new Date(frm.doc.current_to_date).getTime(); + let fr_dt = new Date(frm.doc.from_date).getTime(); + let to_dt = new Date(frm.doc.to_date).getTime(); + let bisect_start = new Date(frm.doc.current_from_date).getTime(); + let bisect_end = new Date(frm.doc.current_to_date).getTime(); + for(let x=fr_dt; x <= to_dt; x+=msiad){ let epoch_in_seconds = x/1000; - datapoints[epoch_in_seconds] = 1; + if ((bisect_start <= x) && (x <= bisect_end )) { + datapoints[epoch_in_seconds] = 1.0; + } else { + datapoints[epoch_in_seconds] = 0.0; + } } console.log(datapoints); @@ -45,7 +52,7 @@ frappe.ui.form.on("Bisect Accounting Statements", { end: new Date(frm.doc.to_date), }, countLabel: 'Difference', - discreteDomains: 1 + discreteDomains: 1, }); }, bisect_left(frm) { diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json index 9a7242fe922a..84d021d7da59 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json @@ -58,23 +58,25 @@ { "fieldname": "current_node", "fieldtype": "Link", - "hidden": 1, "label": "Current Node", "options": "Nodes" }, { "fieldname": "section_break_hmsy", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hidden": 1 }, { "fieldname": "current_from_date", "fieldtype": "Datetime", - "label": "Current From Date" + "label": "Current From Date", + "read_only": 1 }, { "fieldname": "current_to_date", "fieldtype": "Datetime", - "label": "Current To Date" + "label": "Current To Date", + "read_only": 1 }, { "fieldname": "column_break_uqyd", @@ -87,17 +89,20 @@ { "fieldname": "p_l_summary", "fieldtype": "Data", - "label": "P&L Summary" + "label": "P&L Summary", + "read_only": 1 }, { "fieldname": "b_s_summary", "fieldtype": "Data", - "label": "Balance Sheet Summary" + "label": "Balance Sheet Summary", + "read_only": 1 }, { "fieldname": "difference", "fieldtype": "Data", - "label": "Difference" + "label": "Difference", + "read_only": 1 }, { "fieldname": "column_break_aivo", @@ -123,7 +128,8 @@ }, { "fieldname": "section_break_8ph9", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hidden": 1 }, { "fieldname": "bisect_heatmap", @@ -135,7 +141,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-09-27 12:10:40.044935", + "modified": "2023-09-27 14:32:09.962067", "modified_by": "Administrator", "module": "Accounts", "name": "Bisect Accounting Statements", From a9a510547e7858656ccf78ea9fe8568c187d983e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 27 Sep 2023 14:44:48 +0530 Subject: [PATCH 76/94] chore: rename btree and remove debugging statements (cherry picked from commit 6492019383a56669869457d3b6b8cdb2083f7df7) --- .../bisect_accounting_statements.js | 5 ---- .../bisect_accounting_statements.json | 7 ++--- .../bisect_accounting_statements.py | 28 +++++++++---------- .../{nodes => bisect_nodes}/__init__.py | 0 .../nodes.js => bisect_nodes/bisect_nodes.js} | 2 +- .../bisect_nodes.json} | 12 ++++---- .../nodes.py => bisect_nodes/bisect_nodes.py} | 2 +- .../test_bisect_nodes.py} | 2 +- 8 files changed, 26 insertions(+), 32 deletions(-) rename erpnext/accounts/doctype/{nodes => bisect_nodes}/__init__.py (100%) rename erpnext/accounts/doctype/{nodes/nodes.js => bisect_nodes/bisect_nodes.js} (80%) rename erpnext/accounts/doctype/{nodes/nodes.json => bisect_nodes/bisect_nodes.json} (88%) rename erpnext/accounts/doctype/{nodes/nodes.py => bisect_nodes/bisect_nodes.py} (86%) rename erpnext/accounts/doctype/{nodes/test_nodes.py => bisect_nodes/test_bisect_nodes.py} (80%) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js index c00e49d3805f..e9fc330a7090 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js @@ -41,7 +41,6 @@ frappe.ui.form.on("Bisect Accounting Statements", { datapoints[epoch_in_seconds] = 0.0; } } - console.log(datapoints); new frappe.Chart(".bisect_heatmap_location", { type: "heatmap", @@ -61,7 +60,6 @@ frappe.ui.form.on("Bisect Accounting Statements", { method: 'bisect_left', callback: (r) => { frm.trigger("render_heatmap"); - console.log(r); } }); }, @@ -71,7 +69,6 @@ frappe.ui.form.on("Bisect Accounting Statements", { method: 'bisect_right', callback: (r) => { frm.trigger("render_heatmap"); - console.log(r); } }); }, @@ -81,7 +78,6 @@ frappe.ui.form.on("Bisect Accounting Statements", { method: 'move_up', callback: (r) => { frm.trigger("render_heatmap"); - console.log(r); } }); }, @@ -91,7 +87,6 @@ frappe.ui.form.on("Bisect Accounting Statements", { method: 'build_tree', callback: (r) => { frm.trigger("render_heatmap"); - console.log(r); } }); }, diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json index 84d021d7da59..f62d39377e50 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json @@ -59,7 +59,7 @@ "fieldname": "current_node", "fieldtype": "Link", "label": "Current Node", - "options": "Nodes" + "options": "Bisect Nodes" }, { "fieldname": "section_break_hmsy", @@ -128,8 +128,7 @@ }, { "fieldname": "section_break_8ph9", - "fieldtype": "Section Break", - "hidden": 1 + "fieldtype": "Section Break" }, { "fieldname": "bisect_heatmap", @@ -141,7 +140,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-09-27 14:32:09.962067", + "modified": "2023-09-27 15:05:44.285832", "modified_by": "Administrator", "module": "Accounts", "name": "Bisect Accounting Statements", diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py index d2b60a422519..58b874862872 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py @@ -27,7 +27,7 @@ def validate_dates(self): def bfs(self, from_date: datetime, to_date: datetime): # Make Root node - node = frappe.new_doc("Nodes") + node = frappe.new_doc("Bisect Nodes") node.root = None node.period_from_date = from_date node.period_to_date = to_date @@ -42,7 +42,7 @@ def bfs(self, from_date: datetime, to_date: datetime): else: cur_floor = floor(delta.days / 2) next_to_date = cur_node.period_from_date + relativedelta(days=+cur_floor) - left_node = frappe.new_doc("Nodes") + left_node = frappe.new_doc("Bisect Nodes") left_node.period_from_date = cur_node.period_from_date left_node.period_to_date = next_to_date left_node.root = cur_node.name @@ -51,7 +51,7 @@ def bfs(self, from_date: datetime, to_date: datetime): period_queue.append(left_node) next_from_date = cur_node.period_from_date + relativedelta(days=+(cur_floor + 1)) - right_node = frappe.new_doc("Nodes") + right_node = frappe.new_doc("Bisect Nodes") right_node.period_from_date = next_from_date right_node.period_to_date = cur_node.period_to_date right_node.root = cur_node.name @@ -63,7 +63,7 @@ def bfs(self, from_date: datetime, to_date: datetime): def dfs(self, from_date: datetime, to_date: datetime): # Make Root node - node = frappe.new_doc("Nodes") + node = frappe.new_doc("Bisect Nodes") node.root = None node.period_from_date = from_date node.period_to_date = to_date @@ -78,7 +78,7 @@ def dfs(self, from_date: datetime, to_date: datetime): else: cur_floor = floor(delta.days / 2) next_to_date = cur_node.period_from_date + relativedelta(days=+cur_floor) - left_node = frappe.new_doc("Nodes") + left_node = frappe.new_doc("Bisect Nodes") left_node.period_from_date = cur_node.period_from_date left_node.period_to_date = next_to_date left_node.root = cur_node.name @@ -87,7 +87,7 @@ def dfs(self, from_date: datetime, to_date: datetime): period_stack.append(left_node) next_from_date = cur_node.period_from_date + relativedelta(days=+(cur_floor + 1)) - right_node = frappe.new_doc("Nodes") + right_node = frappe.new_doc("Bisect Nodes") right_node.period_from_date = next_from_date right_node.period_to_date = cur_node.period_to_date right_node.root = cur_node.name @@ -99,7 +99,7 @@ def dfs(self, from_date: datetime, to_date: datetime): @frappe.whitelist() def build_tree(self): - frappe.db.delete("Nodes") + frappe.db.delete("Bisect Nodes") # Convert str to datetime format dt_format = guess_date_format(self.from_date) @@ -113,7 +113,7 @@ def build_tree(self): self.dfs(from_date, to_date) # set root as current node - root = frappe.db.get_all("Nodes", filters={"root": ["is", "not set"]})[0] + root = frappe.db.get_all("Bisect Nodes", filters={"root": ["is", "not set"]})[0] frappe.db.set_single_value("Bisect Accounting Statements", "current_node", root.name) def get_report_summary(self): @@ -133,9 +133,9 @@ def get_report_summary(self): @frappe.whitelist() def bisect_left(self): if self.current_node is not None: - cur_node = frappe.get_doc("Nodes", self.current_node) + cur_node = frappe.get_doc("Bisect Nodes", self.current_node) if cur_node.left_child is not None: - lft_node = frappe.get_doc("Nodes", cur_node.left_child) + lft_node = frappe.get_doc("Bisect Nodes", cur_node.left_child) self.current_node = cur_node.left_child self.current_from_date = lft_node.period_from_date self.current_to_date = lft_node.period_to_date @@ -147,9 +147,9 @@ def bisect_left(self): @frappe.whitelist() def bisect_right(self): if self.current_node is not None: - cur_node = frappe.get_doc("Nodes", self.current_node) + cur_node = frappe.get_doc("Bisect Nodes", self.current_node) if cur_node.right_child is not None: - rgt_node = frappe.get_doc("Nodes", cur_node.right_child) + rgt_node = frappe.get_doc("Bisect Nodes", cur_node.right_child) self.current_node = cur_node.right_child self.current_from_date = rgt_node.period_from_date self.current_to_date = rgt_node.period_to_date @@ -161,9 +161,9 @@ def bisect_right(self): @frappe.whitelist() def move_up(self): if self.current_node is not None: - cur_node = frappe.get_doc("Nodes", self.current_node) + cur_node = frappe.get_doc("Bisect Nodes", self.current_node) if cur_node.root is not None: - root = frappe.get_doc("Nodes", cur_node.root) + root = frappe.get_doc("Bisect Nodes", cur_node.root) self.current_node = cur_node.root self.current_from_date = root.period_from_date self.current_to_date = root.period_to_date diff --git a/erpnext/accounts/doctype/nodes/__init__.py b/erpnext/accounts/doctype/bisect_nodes/__init__.py similarity index 100% rename from erpnext/accounts/doctype/nodes/__init__.py rename to erpnext/accounts/doctype/bisect_nodes/__init__.py diff --git a/erpnext/accounts/doctype/nodes/nodes.js b/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.js similarity index 80% rename from erpnext/accounts/doctype/nodes/nodes.js rename to erpnext/accounts/doctype/bisect_nodes/bisect_nodes.js index bd74d68637aa..6dea25fc924b 100644 --- a/erpnext/accounts/doctype/nodes/nodes.js +++ b/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.js @@ -1,7 +1,7 @@ // Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -// frappe.ui.form.on("Nodes", { +// frappe.ui.form.on("Bisect Nodes", { // refresh(frm) { // }, diff --git a/erpnext/accounts/doctype/nodes/nodes.json b/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.json similarity index 88% rename from erpnext/accounts/doctype/nodes/nodes.json rename to erpnext/accounts/doctype/bisect_nodes/bisect_nodes.json index 1238c8136afe..db4170479500 100644 --- a/erpnext/accounts/doctype/nodes/nodes.json +++ b/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.json @@ -1,7 +1,7 @@ { "actions": [], "autoname": "autoincrement", - "creation": "2023-09-25 22:01:33.961832", + "creation": "2023-09-27 14:56:38.112462", "default_view": "List", "doctype": "DocType", "editable_grid": 1, @@ -21,19 +21,19 @@ "fieldname": "root", "fieldtype": "Link", "label": "Root", - "options": "Nodes" + "options": "Bisect Nodes" }, { "fieldname": "left_child", "fieldtype": "Link", "label": "Left Child", - "options": "Nodes" + "options": "Bisect Nodes" }, { "fieldname": "right_child", "fieldtype": "Link", "label": "Right Child", - "options": "Nodes" + "options": "Bisect Nodes" }, { "fieldname": "period_from_date", @@ -63,10 +63,10 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-09-25 22:05:49.577861", + "modified": "2023-09-27 15:09:27.715523", "modified_by": "Administrator", "module": "Accounts", - "name": "Nodes", + "name": "Bisect Nodes", "naming_rule": "Autoincrement", "owner": "Administrator", "permissions": [ diff --git a/erpnext/accounts/doctype/nodes/nodes.py b/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.py similarity index 86% rename from erpnext/accounts/doctype/nodes/nodes.py rename to erpnext/accounts/doctype/bisect_nodes/bisect_nodes.py index 67d5d69bfe7d..0d176f974a87 100644 --- a/erpnext/accounts/doctype/nodes/nodes.py +++ b/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.py @@ -5,5 +5,5 @@ from frappe.model.document import Document -class Nodes(Document): +class BisectNodes(Document): pass diff --git a/erpnext/accounts/doctype/nodes/test_nodes.py b/erpnext/accounts/doctype/bisect_nodes/test_bisect_nodes.py similarity index 80% rename from erpnext/accounts/doctype/nodes/test_nodes.py rename to erpnext/accounts/doctype/bisect_nodes/test_bisect_nodes.py index feeef765e034..5399df139f13 100644 --- a/erpnext/accounts/doctype/nodes/test_nodes.py +++ b/erpnext/accounts/doctype/bisect_nodes/test_bisect_nodes.py @@ -5,5 +5,5 @@ from frappe.tests.utils import FrappeTestCase -class TestNodes(FrappeTestCase): +class TestBisectNodes(FrappeTestCase): pass From f9c30968bf91efe9cd9d6990d67f705d7780f0f7 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 27 Sep 2023 15:10:42 +0530 Subject: [PATCH 77/94] chore: hide internal variables section (cherry picked from commit c4c3090f46af16f52b34dab116b49856b9da2025) --- .../bisect_accounting_statements.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json index f62d39377e50..4ad03528c51a 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json @@ -128,7 +128,8 @@ }, { "fieldname": "section_break_8ph9", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hidden": 1 }, { "fieldname": "bisect_heatmap", @@ -140,7 +141,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-09-27 15:05:44.285832", + "modified": "2023-09-27 15:10:36.394474", "modified_by": "Administrator", "module": "Accounts", "name": "Bisect Accounting Statements", From ebea0fd203ec082c0361ee4a30bb9d44ce011245 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 27 Sep 2023 15:27:27 +0530 Subject: [PATCH 78/94] chore: code cleanup (cherry picked from commit 5e2d21c033fdf6147ec58286d08cac75d6fedf23) --- .../bisect_accounting_statements.js | 2 +- .../bisect_accounting_statements.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js index e9fc330a7090..486630e16240 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js @@ -50,7 +50,7 @@ frappe.ui.form.on("Bisect Accounting Statements", { start: new Date(frm.doc.from_date), end: new Date(frm.doc.to_date), }, - countLabel: 'Difference', + countLabel: 'Bisecting', discreteDomains: 1, }); }, diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py index 58b874862872..0a1de6414fe3 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py @@ -114,7 +114,11 @@ def build_tree(self): # set root as current node root = frappe.db.get_all("Bisect Nodes", filters={"root": ["is", "not set"]})[0] - frappe.db.set_single_value("Bisect Accounting Statements", "current_node", root.name) + self.get_report_summary() + self.current_node = root.name + self.current_from_date = self.from_date + self.current_to_date = self.to_date + self.save() def get_report_summary(self): filters = { From a76d19fe8fdada53dee991bbd2c2a15f547a0d29 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 27 Sep 2023 15:41:53 +0530 Subject: [PATCH 79/94] chore: add screen freeze on wait (cherry picked from commit 395299803f3d5818fa0e17bac95f515fe06b2a09) --- .../bisect_accounting_statements.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js index 486630e16240..519246b9bb41 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js @@ -58,6 +58,8 @@ frappe.ui.form.on("Bisect Accounting Statements", { frm.call({ doc: frm.doc, method: 'bisect_left', + freeze: true, + freeze_message: __("Bisecting Left ..."), callback: (r) => { frm.trigger("render_heatmap"); } @@ -66,6 +68,8 @@ frappe.ui.form.on("Bisect Accounting Statements", { bisect_right(frm) { frm.call({ doc: frm.doc, + freeze: true, + freeze_message: __("Bisecting Right ..."), method: 'bisect_right', callback: (r) => { frm.trigger("render_heatmap"); @@ -75,6 +79,8 @@ frappe.ui.form.on("Bisect Accounting Statements", { move_up(frm) { frm.call({ doc: frm.doc, + freeze: true, + freeze_message: __("Moving up in tree ..."), method: 'move_up', callback: (r) => { frm.trigger("render_heatmap"); @@ -84,6 +90,8 @@ frappe.ui.form.on("Bisect Accounting Statements", { build_tree(frm) { frm.call({ doc: frm.doc, + freeze: true, + freeze_message: __("Rebuilding BTree for period ..."), method: 'build_tree', callback: (r) => { frm.trigger("render_heatmap"); From 84609abd65d93cb701bb3d2ad346451173eb20c4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 27 Sep 2023 15:53:19 +0530 Subject: [PATCH 80/94] chore: UI cleanup (cherry picked from commit ea3071db6643f7802611e7a0ae8857d75583dd96) --- .../bisect_accounting_statements.json | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json index 4ad03528c51a..a9fa7f0695b7 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json @@ -16,12 +16,12 @@ "algorithm", "section_break_8ph9", "current_node", + "section_break_ngid", + "bisect_heatmap", "section_break_hmsy", "current_from_date", "column_break_uqyd", "current_to_date", - "section_break_ngid", - "bisect_heatmap", "section_break_hbyo", "p_l_summary", "column_break_aivo", @@ -63,19 +63,18 @@ }, { "fieldname": "section_break_hmsy", - "fieldtype": "Section Break", - "hidden": 1 + "fieldtype": "Section Break" }, { "fieldname": "current_from_date", "fieldtype": "Datetime", - "label": "Current From Date", + "label": "Bisecting From", "read_only": 1 }, { "fieldname": "current_to_date", "fieldtype": "Datetime", - "label": "Current To Date", + "label": "Bisecting To", "read_only": 1 }, { @@ -141,7 +140,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-09-27 15:10:36.394474", + "modified": "2023-09-27 15:52:14.083727", "modified_by": "Administrator", "module": "Accounts", "name": "Bisect Accounting Statements", From cd0a7fed4137889059782cc79a2d7e74285c04d7 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 27 Sep 2023 16:05:14 +0530 Subject: [PATCH 81/94] refactor: adding labels to important section (cherry picked from commit 993e2bfbf9f75958d5283fcbbcaa5e6f80e88f86) --- .../bisect_accounting_statements.js | 1 - .../bisect_accounting_statements.json | 37 ++++++++++++++++--- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js index 519246b9bb41..ece0fb33e544 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js @@ -44,7 +44,6 @@ frappe.ui.form.on("Bisect Accounting Statements", { new frappe.Chart(".bisect_heatmap_location", { type: "heatmap", - title: "Bisecting On", data: { dataPoints: datapoints, start: new Date(frm.doc.from_date), diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json index a9fa7f0695b7..b66c17b6489d 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json @@ -19,14 +19,19 @@ "section_break_ngid", "bisect_heatmap", "section_break_hmsy", + "bisecting_from", "current_from_date", "column_break_uqyd", + "bisecting_to", "current_to_date", "section_break_hbyo", + "heading_cppb", "p_l_summary", "column_break_aivo", + "balance_sheet_summary", "b_s_summary", "column_break_gvwx", + "difference_heading", "difference" ], "fields": [ @@ -68,13 +73,11 @@ { "fieldname": "current_from_date", "fieldtype": "Datetime", - "label": "Bisecting From", "read_only": 1 }, { "fieldname": "current_to_date", "fieldtype": "Datetime", - "label": "Bisecting To", "read_only": 1 }, { @@ -88,19 +91,16 @@ { "fieldname": "p_l_summary", "fieldtype": "Data", - "label": "P&L Summary", "read_only": 1 }, { "fieldname": "b_s_summary", "fieldtype": "Data", - "label": "Balance Sheet Summary", "read_only": 1 }, { "fieldname": "difference", "fieldtype": "Data", - "label": "Difference", "read_only": 1 }, { @@ -134,13 +134,38 @@ "fieldname": "bisect_heatmap", "fieldtype": "HTML", "label": "Heatmap" + }, + { + "fieldname": "heading_cppb", + "fieldtype": "Heading", + "label": "Profit and Loss Summary" + }, + { + "fieldname": "balance_sheet_summary", + "fieldtype": "Heading", + "label": "Balance Sheet Summary" + }, + { + "fieldname": "difference_heading", + "fieldtype": "Heading", + "label": "Difference" + }, + { + "fieldname": "bisecting_from", + "fieldtype": "Heading", + "label": "Bisecting From" + }, + { + "fieldname": "bisecting_to", + "fieldtype": "Heading", + "label": "Bisecting To" } ], "hide_toolbar": 1, "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-09-27 15:52:14.083727", + "modified": "2023-09-27 16:08:18.155873", "modified_by": "Administrator", "module": "Accounts", "name": "Bisect Accounting Statements", From 54e7f303e9d11b3bc0e43279c3ccc24b4e53cf8a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 27 Sep 2023 16:22:20 +0530 Subject: [PATCH 82/94] chore: change data type for summary fields (cherry picked from commit 228aa1a244632505bb466f1252c14ce5950b1ef7) --- .../bisect_accounting_statements.json | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json index b66c17b6489d..d70ae1f90e4b 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json @@ -7,6 +7,7 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "section_break_cvfg", "company", "column_break_hcam", "from_date", @@ -90,17 +91,17 @@ }, { "fieldname": "p_l_summary", - "fieldtype": "Data", + "fieldtype": "Float", "read_only": 1 }, { "fieldname": "b_s_summary", - "fieldtype": "Data", + "fieldtype": "Float", "read_only": 1 }, { "fieldname": "difference", - "fieldtype": "Data", + "fieldtype": "Float", "read_only": 1 }, { @@ -159,13 +160,17 @@ "fieldname": "bisecting_to", "fieldtype": "Heading", "label": "Bisecting To" + }, + { + "fieldname": "section_break_cvfg", + "fieldtype": "Section Break" } ], "hide_toolbar": 1, "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-09-27 16:08:18.155873", + "modified": "2023-09-27 16:22:10.670836", "modified_by": "Administrator", "module": "Accounts", "name": "Bisect Accounting Statements", From 3cab242e9f3beedd6e7e817658284a91bde34f07 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 1 Dec 2023 16:50:17 +0530 Subject: [PATCH 83/94] chore: restrict only to administrator and type info (cherry picked from commit 90c6d4dc85701902ad9d12b31716db098831dc10) --- .../bisect_accounting_statements.json | 5 +++-- .../bisect_accounting_statements.py | 20 +++++++++++++++++++ .../doctype/bisect_nodes/bisect_nodes.json | 5 +++-- .../doctype/bisect_nodes/bisect_nodes.py | 19 ++++++++++++++++++ 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json index d70ae1f90e4b..e129fa60c2c4 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json @@ -170,7 +170,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-09-27 16:22:10.670836", + "modified": "2023-12-01 16:49:54.073890", "modified_by": "Administrator", "module": "Accounts", "name": "Bisect Accounting Statements", @@ -182,11 +182,12 @@ "email": 1, "print": 1, "read": 1, - "role": "System Manager", + "role": "Administrator", "share": 1, "write": 1 } ], + "read_only": 1, "sort_field": "modified", "sort_order": "DESC", "states": [] diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py index 0a1de6414fe3..e2a67d14b798 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py @@ -14,6 +14,26 @@ class BisectAccountingStatements(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + algorithm: DF.Literal["BFS", "DFS"] + b_s_summary: DF.Float + company: DF.Link | None + current_from_date: DF.Datetime | None + current_node: DF.Link | None + current_to_date: DF.Datetime | None + difference: DF.Float + from_date: DF.Datetime | None + p_l_summary: DF.Float + to_date: DF.Datetime | None + # end: auto-generated types + def validate(self): self.validate_dates() diff --git a/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.json b/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.json index db4170479500..f352d7a31a11 100644 --- a/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.json +++ b/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.json @@ -63,7 +63,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-09-27 15:09:27.715523", + "modified": "2023-12-01 16:49:57.146867", "modified_by": "Administrator", "module": "Accounts", "name": "Bisect Nodes", @@ -78,11 +78,12 @@ "print": 1, "read": 1, "report": 1, - "role": "System Manager", + "role": "Administrator", "share": 1, "write": 1 } ], + "read_only": 1, "sort_field": "modified", "sort_order": "DESC", "states": [] diff --git a/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.py b/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.py index 0d176f974a87..e49eabbff550 100644 --- a/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.py +++ b/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.py @@ -6,4 +6,23 @@ class BisectNodes(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + balance_sheet_summary: DF.Float + difference: DF.Float + left_child: DF.Link | None + name: DF.Int | None + period_from_date: DF.Datetime | None + period_to_date: DF.Datetime | None + profit_loss_summary: DF.Float + right_child: DF.Link | None + root: DF.Link | None + # end: auto-generated types + pass From 5d90b0fd1dea2895d69b8fcfbd99ab63aa6b26f8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 1 Dec 2023 17:07:39 +0530 Subject: [PATCH 84/94] refactor: save results in node (cherry picked from commit ca14ae8f1bc69539ca3a2f49f92e2b6c667a40b3) --- .../bisect_accounting_statements.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py index e2a67d14b798..216f55308465 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py @@ -154,6 +154,13 @@ def get_report_summary(self): self.b_s_summary = bs_summary.execute_script_report(filters=filters)[5] self.difference = abs(self.p_l_summary - self.b_s_summary) + def update_node(self): + current_node = frappe.get_doc("Bisect Nodes", self.current_node) + current_node.balance_sheet_summary = self.b_s_summary + current_node.profit_loss_summary = self.p_l_summary + current_node.difference = self.difference + current_node.save() + @frappe.whitelist() def bisect_left(self): if self.current_node is not None: @@ -164,6 +171,7 @@ def bisect_left(self): self.current_from_date = lft_node.period_from_date self.current_to_date = lft_node.period_to_date self.get_report_summary() + self.update_node() self.save() else: frappe.msgprint("No more children on Left") @@ -178,6 +186,7 @@ def bisect_right(self): self.current_from_date = rgt_node.period_from_date self.current_to_date = rgt_node.period_to_date self.get_report_summary() + self.update_node() self.save() else: frappe.msgprint("No more children on Right") @@ -192,6 +201,7 @@ def move_up(self): self.current_from_date = root.period_from_date self.current_to_date = root.period_to_date self.get_report_summary() + self.update_node() self.save() else: frappe.msgprint("Reached Root") From 48bfb9d31528473922b050b55616128a798deb3e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 1 Dec 2023 17:46:28 +0530 Subject: [PATCH 85/94] refactor: flag to differentiate generated and default values (cherry picked from commit 0925706d5efac2cbdfe5240e0310c1a9738a3560) --- .../accounts/doctype/bisect_nodes/bisect_nodes.json | 11 +++++++++-- erpnext/accounts/doctype/bisect_nodes/bisect_nodes.py | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.json b/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.json index f352d7a31a11..03fad261c3cc 100644 --- a/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.json +++ b/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.json @@ -14,7 +14,8 @@ "period_to_date", "difference", "balance_sheet_summary", - "profit_loss_summary" + "profit_loss_summary", + "generated" ], "fields": [ { @@ -59,11 +60,17 @@ "fieldname": "profit_loss_summary", "fieldtype": "Float", "label": "Profit and Loss Summary" + }, + { + "default": "0", + "fieldname": "generated", + "fieldtype": "Check", + "label": "Generated" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-12-01 16:49:57.146867", + "modified": "2023-12-01 17:46:12.437996", "modified_by": "Administrator", "module": "Accounts", "name": "Bisect Nodes", diff --git a/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.py b/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.py index e49eabbff550..f50776641d0e 100644 --- a/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.py +++ b/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.py @@ -16,6 +16,7 @@ class BisectNodes(Document): balance_sheet_summary: DF.Float difference: DF.Float + generated: DF.Check left_child: DF.Link | None name: DF.Int | None period_from_date: DF.Datetime | None From 3a38aacc305b2a731b7964d49362d19ed49a0e03 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 1 Dec 2023 17:52:36 +0530 Subject: [PATCH 86/94] refactor: cache results (cherry picked from commit 14c8c8c33d8f8df636ed5b3de589cc872f6aaf7b) --- .../bisect_accounting_statements.py | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py index 216f55308465..1d6fd621aa06 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py @@ -66,6 +66,7 @@ def bfs(self, from_date: datetime, to_date: datetime): left_node.period_from_date = cur_node.period_from_date left_node.period_to_date = next_to_date left_node.root = cur_node.name + left_node.generated = False left_node.insert() cur_node.left_child = left_node.name period_queue.append(left_node) @@ -75,6 +76,7 @@ def bfs(self, from_date: datetime, to_date: datetime): right_node.period_from_date = next_from_date right_node.period_to_date = cur_node.period_to_date right_node.root = cur_node.name + right_node.generated = False right_node.insert() cur_node.right_child = right_node.name period_queue.append(right_node) @@ -102,6 +104,7 @@ def dfs(self, from_date: datetime, to_date: datetime): left_node.period_from_date = cur_node.period_from_date left_node.period_to_date = next_to_date left_node.root = cur_node.name + left_node.generated = False left_node.insert() cur_node.left_child = left_node.name period_stack.append(left_node) @@ -111,6 +114,7 @@ def dfs(self, from_date: datetime, to_date: datetime): right_node.period_from_date = next_from_date right_node.period_to_date = cur_node.period_to_date right_node.root = cur_node.name + right_node.generated = False right_node.insert() cur_node.right_child = right_node.name period_stack.append(right_node) @@ -159,8 +163,26 @@ def update_node(self): current_node.balance_sheet_summary = self.b_s_summary current_node.profit_loss_summary = self.p_l_summary current_node.difference = self.difference + current_node.generated = True current_node.save() + def current_node_has_summary_info(self): + "Assertion method" + return frappe.db.get_value("Bisect Nodes", self.current_node, "generated") + + def fetch_summary_info_from_current_node(self): + current_node = frappe.get_doc("Bisect Nodes", self.current_node) + self.p_l_summary = current_node.balance_sheet_summary + self.b_s_summary = current_node.profit_loss_summary + self.difference = abs(self.p_l_summary - self.b_s_summary) + + def fetch_or_calculate(self): + if self.current_node_has_summary_info(): + self.fetch_summary_info_from_current_node() + else: + self.get_report_summary() + self.update_node() + @frappe.whitelist() def bisect_left(self): if self.current_node is not None: @@ -170,8 +192,7 @@ def bisect_left(self): self.current_node = cur_node.left_child self.current_from_date = lft_node.period_from_date self.current_to_date = lft_node.period_to_date - self.get_report_summary() - self.update_node() + self.fetch_or_calculate() self.save() else: frappe.msgprint("No more children on Left") @@ -185,8 +206,7 @@ def bisect_right(self): self.current_node = cur_node.right_child self.current_from_date = rgt_node.period_from_date self.current_to_date = rgt_node.period_to_date - self.get_report_summary() - self.update_node() + self.fetch_or_calculate() self.save() else: frappe.msgprint("No more children on Right") @@ -200,8 +220,7 @@ def move_up(self): self.current_node = cur_node.root self.current_from_date = root.period_from_date self.current_to_date = root.period_to_date - self.get_report_summary() - self.update_node() + self.fetch_or_calculate() self.save() else: frappe.msgprint("Reached Root") From 4eb1fbfe4288e849ca01d1d49c39540af7a99300 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 27 Dec 2023 17:31:34 +0530 Subject: [PATCH 87/94] chore: resolve linter issues (cherry picked from commit 0890b414b14d67803abe2ee5be72ebdcf9f6bee4) --- .../bisect_accounting_statements.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py index 1d6fd621aa06..da273b9f8917 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py @@ -195,7 +195,7 @@ def bisect_left(self): self.fetch_or_calculate() self.save() else: - frappe.msgprint("No more children on Left") + frappe.msgprint(_("No more children on Left")) @frappe.whitelist() def bisect_right(self): @@ -209,7 +209,7 @@ def bisect_right(self): self.fetch_or_calculate() self.save() else: - frappe.msgprint("No more children on Right") + frappe.msgprint(_("No more children on Right")) @frappe.whitelist() def move_up(self): @@ -223,4 +223,4 @@ def move_up(self): self.fetch_or_calculate() self.save() else: - frappe.msgprint("Reached Root") + frappe.msgprint(_("Reached Root")) From 9f3b6d21c3063a5df77d4c6441f870ae962f00f8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 22 Mar 2024 14:42:57 +0530 Subject: [PATCH 88/94] chore: resolve JS linter issue --- .../bisect_accounting_statements.js | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js index ece0fb33e544..f3532e5be61c 100644 --- a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js +++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js @@ -6,18 +6,18 @@ frappe.ui.form.on("Bisect Accounting Statements", { frm.trigger("render_heatmap"); }, refresh(frm) { - frm.add_custom_button(__('Bisect Left'), () => { + frm.add_custom_button(__("Bisect Left"), () => { frm.trigger("bisect_left"); }); - frm.add_custom_button(__('Bisect Right'), () => { + frm.add_custom_button(__("Bisect Right"), () => { frm.trigger("bisect_right"); }); - frm.add_custom_button(__('Up'), () => { + frm.add_custom_button(__("Up"), () => { frm.trigger("move_up"); }); - frm.add_custom_button(__('Build Tree'), () => { + frm.add_custom_button(__("Build Tree"), () => { frm.trigger("build_tree"); }); }, @@ -26,16 +26,16 @@ frappe.ui.form.on("Bisect Accounting Statements", { bisect_heatmap.addClass("bisect_heatmap_location"); // milliseconds in a day - let msiad=24*60*60*1000; + let msiad = 24 * 60 * 60 * 1000; let datapoints = {}; let fr_dt = new Date(frm.doc.from_date).getTime(); let to_dt = new Date(frm.doc.to_date).getTime(); let bisect_start = new Date(frm.doc.current_from_date).getTime(); let bisect_end = new Date(frm.doc.current_to_date).getTime(); - for(let x=fr_dt; x <= to_dt; x+=msiad){ - let epoch_in_seconds = x/1000; - if ((bisect_start <= x) && (x <= bisect_end )) { + for (let x = fr_dt; x <= to_dt; x += msiad) { + let epoch_in_seconds = x / 1000; + if (bisect_start <= x && x <= bisect_end) { datapoints[epoch_in_seconds] = 1.0; } else { datapoints[epoch_in_seconds] = 0.0; @@ -49,19 +49,19 @@ frappe.ui.form.on("Bisect Accounting Statements", { start: new Date(frm.doc.from_date), end: new Date(frm.doc.to_date), }, - countLabel: 'Bisecting', + countLabel: "Bisecting", discreteDomains: 1, }); }, bisect_left(frm) { frm.call({ doc: frm.doc, - method: 'bisect_left', + method: "bisect_left", freeze: true, freeze_message: __("Bisecting Left ..."), callback: (r) => { frm.trigger("render_heatmap"); - } + }, }); }, bisect_right(frm) { @@ -69,10 +69,10 @@ frappe.ui.form.on("Bisect Accounting Statements", { doc: frm.doc, freeze: true, freeze_message: __("Bisecting Right ..."), - method: 'bisect_right', + method: "bisect_right", callback: (r) => { frm.trigger("render_heatmap"); - } + }, }); }, move_up(frm) { @@ -80,10 +80,10 @@ frappe.ui.form.on("Bisect Accounting Statements", { doc: frm.doc, freeze: true, freeze_message: __("Moving up in tree ..."), - method: 'move_up', + method: "move_up", callback: (r) => { frm.trigger("render_heatmap"); - } + }, }); }, build_tree(frm) { @@ -91,10 +91,10 @@ frappe.ui.form.on("Bisect Accounting Statements", { doc: frm.doc, freeze: true, freeze_message: __("Rebuilding BTree for period ..."), - method: 'build_tree', + method: "build_tree", callback: (r) => { frm.trigger("render_heatmap"); - } + }, }); }, }); From 2f839fbf6dcb04d56a452ccd139d9d2396b1912f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 22 Mar 2024 15:03:38 +0530 Subject: [PATCH 89/94] fix: rate reset to zero (cherry picked from commit 6821baa850e4f820bdd274b5113a34e3fea3daba) --- erpnext/stock/get_item_details.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index aa2878727b08..33abdcb53216 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -860,6 +860,9 @@ def get_price_list_rate(args, item_doc, out=None): if args.price_list and args.rate: insert_item_price(args) + if not price_list_rate: + return out + out.price_list_rate = ( flt(price_list_rate) * flt(args.plc_conversion_rate) / flt(args.conversion_rate) ) From 22cec21c6f9e6edff8d08882584e1fa1338572c6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 22 Mar 2024 16:11:05 +0530 Subject: [PATCH 90/94] refactor: reset flag for old records (cherry picked from commit a88bf8419e7c9987d81b7d0c1d05f45be26d65cc) --- erpnext/patches.txt | 2 +- erpnext/patches/v14_0/update_flag_for_return_invoices.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index e67dad0e8ba3..57f49ef7754e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -355,7 +355,7 @@ execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency" erpnext.patches.v14_0.clear_reconciliation_values_from_singles erpnext.patches.v14_0.update_total_asset_cost_field erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool -erpnext.patches.v14_0.update_flag_for_return_invoices +erpnext.patches.v14_0.update_flag_for_return_invoices #2024-03-22 # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20 diff --git a/erpnext/patches/v14_0/update_flag_for_return_invoices.py b/erpnext/patches/v14_0/update_flag_for_return_invoices.py index feb43beacf8c..bea99575425b 100644 --- a/erpnext/patches/v14_0/update_flag_for_return_invoices.py +++ b/erpnext/patches/v14_0/update_flag_for_return_invoices.py @@ -12,6 +12,10 @@ def execute(): creation_date = "2024-01-25" si = qb.DocType("Sales Invoice") + + # unset flag, as migration would have set it for all records, as the field was introduced with default '1' + qb.update(si).set(si.update_outstanding_for_self, False).run() + if cr_notes := ( qb.from_(si) .select(si.name) @@ -37,6 +41,10 @@ def execute(): ).run() pi = qb.DocType("Purchase Invoice") + + # unset flag, as migration would have set it for all records, as the field was introduced with default '1' + qb.update(pi).set(pi.update_outstanding_for_self, False).run() + if dr_notes := ( qb.from_(pi) .select(pi.name) From dbb4391f1e4c964bd8b2b74e10fba1a7b3110b87 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 22 Mar 2024 17:50:51 +0530 Subject: [PATCH 91/94] refactor: hide on print formats (cherry picked from commit fdcdc8a56ea8892f197dd2a45b175d20137953f6) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index ddb82d95f9de..25f11b65e492 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -2170,7 +2170,8 @@ "fieldname": "update_outstanding_for_self", "fieldtype": "Check", "label": "Update Outstanding for Self", - "no_copy": 1 + "no_copy": 1, + "print_hide": 1 } ], "icon": "fa fa-file-text", @@ -2183,7 +2184,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2024-03-20 16:02:52.237732", + "modified": "2024-03-22 17:50:34.395602", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", From c3f93384301c64e0b38df7ca291d0a8f8d01244d Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 22 Feb 2024 09:22:05 +0530 Subject: [PATCH 92/94] fix: remove microsecond from posting datetime (#40017) (cherry picked from commit 0b04d04da3bb876080cfc25b8cd889416295ea8f) --- erpnext/patches.txt | 2 +- .../v14_0/update_posting_datetime_and_dropped_indexes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index e67dad0e8ba3..d95f44c4a2ac 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -274,7 +274,7 @@ erpnext.patches.v14_0.clear_reconciliation_values_from_singles [post_model_sync] execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings') -erpnext.patches.v14_0.update_posting_datetime_and_dropped_indexes +erpnext.patches.v14_0.update_posting_datetime_and_dropped_indexes #22-02-2024 erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents erpnext.patches.v14_0.delete_shopify_doctypes erpnext.patches.v14_0.delete_healthcare_doctypes diff --git a/erpnext/patches/v14_0/update_posting_datetime_and_dropped_indexes.py b/erpnext/patches/v14_0/update_posting_datetime_and_dropped_indexes.py index 6ec3f842007e..ca126a40a409 100644 --- a/erpnext/patches/v14_0/update_posting_datetime_and_dropped_indexes.py +++ b/erpnext/patches/v14_0/update_posting_datetime_and_dropped_indexes.py @@ -5,7 +5,7 @@ def execute(): frappe.db.sql( """ UPDATE `tabStock Ledger Entry` - SET posting_datetime = timestamp(posting_date, posting_time) + SET posting_datetime = DATE_FORMAT(timestamp(posting_date, posting_time), '%Y-%m-%d %H:%i:%s') """ ) From 44851212551b48245b656633588704e31e89d437 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 24 Mar 2024 19:02:38 +0530 Subject: [PATCH 93/94] fix: Add default finance book check in P&L statement (cherry picked from commit 5be3417fdf09327f9ce2e5b5cff1928a96c0c07b) --- .../profit_and_loss_statement/profit_and_loss_statement.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js index c89a86c37b60..521625bbcd40 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js @@ -26,3 +26,10 @@ frappe.require("assets/erpnext/js/financial_statements.js", function () { default: 1, }); }); + +frappe.query_reports["Profit and Loss Statement"]["filters"].push({ + fieldname: "include_default_book_entries", + label: __("Include Default FB Entries"), + fieldtype: "Check", + default: 1, +}); From fb252ec29acb51090b93b77ef795985776363300 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 26 Mar 2024 18:16:37 +0530 Subject: [PATCH 94/94] fix: incorrect total qty in job card (cherry picked from commit c3546cf8e20279b23e6bb3fe78cffc01922f9205) --- erpnext/manufacturing/doctype/job_card/job_card.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 5253c5ffda8b..841055e2783f 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -375,7 +375,7 @@ def add_time_log(self, args): { "to_time": get_datetime(args.get("complete_time")), "operation": args.get("sub_operation"), - "completed_qty": args.get("completed_qty") or 0.0, + "completed_qty": (args.get("completed_qty") if last_row.idx == row.idx else 0.0), } ) elif args.get("start_time"):