From a8ac2a088db85251cbf81f6f47b08ed5dca42495 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 7 Jun 2023 15:09:49 +0530 Subject: [PATCH 01/24] chore: extend default role profiles (cherry picked from commit 0166f69b319c1a6722324f3316a47ade77de76f0) --- erpnext/setup/install.py | 42 ++++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index cee7112c1b6..1e047d1e4dd 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -215,13 +215,39 @@ def hide_workspaces(): def create_default_role_profiles(): - for module in ["Accounts", "Stock", "Manufacturing"]: - create_role_profile(module) + for role_profile_name, roles in DEFAULT_ROLE_PROFILES.items(): + role_profile = frappe.new_doc("Role Profile") + role_profile.role_profile = role_profile_name + for role in roles: + role_profile.append("roles", {"role": role}) + + role_profile.insert(ignore_permissions=True) -def create_role_profile(module): - role_profile = frappe.new_doc("Role Profile") - role_profile.role_profile = _("{0} User").format(module) - role_profile.append("roles", {"role": module + " User"}) - role_profile.append("roles", {"role": module + " Manager"}) - role_profile.insert() +DEFAULT_ROLE_PROFILES = { + "Inventory": [ + "Stock User", + "Stock Manager", + "Item Manager", + ], + "Manufacturing": [ + "Stock User", + "Manufacturing User", + "Manufacturing Manager", + ], + "Accounts": [ + "Accounts User", + "Accounts Manager", + ], + "Sales": [ + "Sales User", + "Stock User", + "Sales Manager", + ], + "Purchase": [ + "Item Manager", + "Stock User", + "Purchase User", + "Purchase Manager", + ], +} From e5055160fbd8abe48cf0612eb68fa246caafaed3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 7 Jun 2023 15:27:45 +0530 Subject: [PATCH 02/24] fix: `enqueue_after_commit` wherever it makes sense (backport #35588) (#35590) fix: `enqueue_after_commit` wherever it makes sense (#35588) (cherry picked from commit 4507cb3cd724207f6e0080e00b122113c13f826a) Co-authored-by: Ankush Menat --- .../doctype/accounting_dimension/accounting_dimension.py | 6 ++++-- .../period_closing_voucher/period_closing_voucher.py | 1 + .../manufacturing/doctype/bom_update_log/bom_update_log.py | 2 ++ erpnext/stock/doctype/item/item.py | 1 + erpnext/stock/doctype/stock_settings/stock_settings.py | 1 + 5 files changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index ce1ed336d0c..81ff6a52db1 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -50,13 +50,15 @@ class AccountingDimension(Document): if frappe.flags.in_test: make_dimension_in_accounting_doctypes(doc=self) else: - frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self, queue="long") + frappe.enqueue( + make_dimension_in_accounting_doctypes, doc=self, queue="long", enqueue_after_commit=True + ) def on_trash(self): if frappe.flags.in_test: delete_accounting_dimension(doc=self) else: - frappe.enqueue(delete_accounting_dimension, doc=self, queue="long") + frappe.enqueue(delete_accounting_dimension, doc=self, queue="long", enqueue_after_commit=True) def set_fieldname_and_label(self): if not self.label: 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 46e2c50e40b..9e527d140cb 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -35,6 +35,7 @@ class PeriodClosingVoucher(AccountsController): voucher_type="Period Closing Voucher", voucher_no=self.name, queue="long", + enqueue_after_commit=True, ) frappe.msgprint( _("The GL Entries will be cancelled in the background, it can take a few minutes."), alert=True diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index 7477f9528ec..17b5aae9666 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -88,12 +88,14 @@ class BOMUpdateLog(Document): boms=boms, timeout=40000, now=frappe.flags.in_test, + enqueue_after_commit=True, ) else: frappe.enqueue( method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.process_boms_cost_level_wise", update_doc=self, now=frappe.flags.in_test, + enqueue_after_commit=True, ) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 8a25be58618..ba1c04fe27e 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -714,6 +714,7 @@ class Item(Document): template=self, now=frappe.flags.in_test, timeout=600, + enqueue_after_commit=True, ) def validate_has_variants(self): diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index 50807a96abd..305459116b5 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -93,6 +93,7 @@ class StockSettings(Document): frappe.enqueue( "erpnext.stock.doctype.stock_settings.stock_settings.clean_all_descriptions", now=frappe.flags.in_test, + enqueue_after_commit=True, ) def validate_pending_reposts(self): From dee82754abd186a8acc0602c8aa4d5c71d89aba3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 7 Jun 2023 22:02:03 +0530 Subject: [PATCH 03/24] =?UTF-8?q?fix:=20based=20on=20status=5Fupdate.py=20?= =?UTF-8?q?update=20opportunity=20status=20to=20converted=E2=80=A6=20(#351?= =?UTF-8?q?45)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: based on status_update.py update opportunity status to converted… (#35145) fix: based on status_update.py update opportunity status to converted on sales submit (cherry picked from commit a9a47a51e49aeb84770ce94b62b5966952b56040) Co-authored-by: HarryPaulo --- erpnext/selling/doctype/sales_order/sales_order.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index ceb990d61b2..92b469c6229 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -217,6 +217,7 @@ class SalesOrder(SellingController): frappe.throw(_("Quotation {0} is cancelled").format(quotation)) doc.set_status(update=True) + doc.update_opportunity("Converted" if flag == "submit" else "Quotation") def validate_drop_ship(self): for d in self.get("items"): From 2077f6e89cff76f874ec2af2acd75fddf3f566c1 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 7 Jun 2023 22:04:14 +0530 Subject: [PATCH 04/24] fix: Update de.csv (#35278) fix: Update de.csv (#35278) added many fixes on the base of 'No' which is often wrongly translated to 'Kein'. Also removed translation of Naming Seris like ACC-INV-.YYYY.- to ACC-INV-.YYYY.- (cherry picked from commit c236979508c00f4104fa2f45bcc925634f5905fd) Co-authored-by: Wolfram Schmidt --- erpnext/translations/de.csv | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index ac6ccf99c6e..31963d21d93 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -7638,20 +7638,19 @@ Restaurant Order Entry Item,Restaurantbestellzugangsposten, Served,Serviert, Restaurant Reservation,Restaurant Reservierung, Waitlisted,Auf der Warteliste, -No Show,Keine Show, -No of People,Nein von Menschen, +No Show,Nicht angetreten, +No of People,Anzahl von Personen, Reservation Time,Reservierungszeit, Reservation End Time,Reservierungsendzeit, No of Seats,Anzahl der Sitze, Minimum Seating,Mindestbestuhlung, "Keep Track of Sales Campaigns. Keep track of Leads, Quotations, Sales Order etc from Campaigns to gauge Return on Investment. ","Verkaufskampagne verfolgen: Leads, Angebote, Aufträge usw. von Kampagnen beobachten um die Kapitalverzinsung (RoI) zu messen.", -SAL-CAM-.YYYY.-,SAL-CAM-.YYYY.-, Campaign Schedules,Kampagnenpläne, Buyer of Goods and Services.,Käufer von Waren und Dienstleistungen., -CUST-.YYYY.-,CUST-.YYYY.-, Default Company Bank Account,Standard-Bankkonto des Unternehmens, From Lead,Aus Lead, -Account Manager,Buchhalter, +Account Manager,Kundenberater, +Accounts Manager,Buchhalter, Allow Sales Invoice Creation Without Sales Order,Ermöglichen Sie die Erstellung von Kundenrechnungen ohne Auftrag, Allow Sales Invoice Creation Without Delivery Note,Ermöglichen Sie die Erstellung einer Ausgangsrechnung ohne Lieferschein, Default Price List,Standardpreisliste, @@ -7692,7 +7691,6 @@ Quantity of Items,Anzahl der Artikel, "Aggregate group of **Items** into another **Item**. This is useful if you are bundling a certain **Items** into a package and you maintain stock of the packed **Items** and not the aggregate **Item**. \n\nThe package **Item** will have ""Is Stock Item"" as ""No"" and ""Is Sales Item"" as ""Yes"".\n\nFor Example: If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.\n\nNote: BOM = Bill of Materials","Fassen Sie eine Gruppe von Artikeln zu einem neuen Artikel zusammen. Dies ist nützlich, wenn Sie bestimmte Artikel zu einem Paket bündeln und einen Bestand an Artikel-Bündeln erhalten und nicht einen Bestand der einzelnen Artikel. Das Artikel-Bündel erhält für das Attribut ""Ist Lagerartikel"" den Wert ""Nein"" und für das Attribut ""Ist Verkaufsartikel"" den Wert ""Ja"". Beispiel: Wenn Sie Laptops und Tragetaschen getrennt verkaufen und einen bestimmten Preis anbieten, wenn der Kunde beides zusammen kauft, dann wird der Laptop mit der Tasche zusammen ein neuer Bündel-Artikel. Anmerkung: BOM = Stückliste", Parent Item,Übergeordneter Artikel, List items that form the package.,"Die Artikel auflisten, die das Paket bilden.", -SAL-QTN-.YYYY.-,SAL-QTN-.YYYY.-, Quotation To,Angebot für, Rate at which customer's currency is converted to company's base currency,"Kurs, zu dem die Währung des Kunden in die Basiswährung des Unternehmens umgerechnet wird", Rate at which Price list currency is converted to company's base currency,"Kurs, zu dem die Währung der Preisliste in die Basiswährung des Unternehmens umgerechnet wird", @@ -7704,7 +7702,6 @@ Quotation Item,Angebotsposition, Against Doctype,Zu DocType, Against Docname,Zu Dokumentenname, Additional Notes,Zusätzliche Bemerkungen, -SAL-ORD-.YYYY.-,SAL-ORD-.YYYY.-, Skip Delivery Note,Lieferschein überspringen, In Words will be visible once you save the Sales Order.,"""In Worten"" wird sichtbar, sobald Sie den Auftrag speichern.", Track this Sales Order against any Project,Diesen Auftrag in jedem Projekt nachverfolgen, @@ -7935,7 +7932,7 @@ For reference,Zu Referenzzwecken, Territory Targets,Ziele für die Region, Set Item Group-wise budgets on this Territory. You can also include seasonality by setting the Distribution.,Artikelgruppenbezogene Budgets für diese Region erstellen. Durch Setzen der Auslieferungseinstellungen können auch saisonale Aspekte mit einbezogen werden., UOM Name,Maßeinheit-Name, -Check this to disallow fractions. (for Nos),"Hier aktivieren, um keine Bruchteile zuzulassen (für Nr.)", +Check this to disallow fractions. (for Nos),"Hier aktivieren, um keine Bruchteile zuzulassen (für Anzahl)", Website Item Group,Webseiten-Artikelgruppe, Cross Listing of Item in multiple groups,Kreuzweise Auflistung des Artikels in mehreren Gruppen, Default settings for Shopping Cart,Standardeinstellungen für den Warenkorb, @@ -8016,7 +8013,6 @@ Contact Information,Kontaktinformationen, Email sent to,E-Mail versandt an, Dispatch Information,Versandinformationen, Estimated Arrival,Voraussichtliche Ankunft, -MAT-DT-.YYYY.-,MAT-DT-.YYYY.-, Initial Email Notification Sent,Erste E-Mail-Benachrichtigung gesendet, Delivery Details,Lieferdetails, Driver Email,Fahrer-E-Mail, @@ -8176,7 +8172,6 @@ Purchase Receipt Item,Kaufbeleg-Artikel, Landed Cost Purchase Receipt,Einstandspreis-Kaufbeleg, Landed Cost Taxes and Charges,Einstandspreis Steuern und Gebühren, Landed Cost Voucher,Beleg über Einstandskosten, -MAT-LCV-.YYYY.-,MAT-LCV-.YYYY.-, Purchase Receipts,Kaufbelege, Purchase Receipt Items,Kaufbeleg-Artikel, Get Items From Purchase Receipts,Artikel vom Kaufbeleg übernehmen, @@ -8184,7 +8179,6 @@ Distribute Charges Based On,Kosten auf folgender Grundlage verteilen, Landed Cost Help,Hilfe zum Einstandpreis, Manufacturers used in Items,Hersteller im Artikel verwendet, Limited to 12 characters,Limitiert auf 12 Zeichen, -MAT-MR-.YYYY.-,MAT-MR-.YYYY.-, Partially Ordered,Teilweise bestellt, Transferred,Übergeben, % Ordered,% bestellt, @@ -8199,7 +8193,6 @@ Prevdoc DocType,Prevdoc DocType, Parent Detail docname,Übergeordnetes Detail Dokumentenname, "Generate packing slips for packages to be delivered. Used to notify package number, package contents and its weight.","Packzettel für zu liefernde Pakete generieren. Wird verwendet, um Paketnummer, Packungsinhalt und das Gewicht zu dokumentieren.", Indicates that the package is a part of this delivery (Only Draft),"Zeigt an, dass das Paket ein Teil dieser Lieferung ist (nur Entwurf)", -MAT-PAC-.YYYY.-,MAT-PAC-.YYYY.-, From Package No.,Von Paket Nr., Identification of the package for the delivery (for print),Kennzeichnung des Paketes für die Lieferung (für den Druck), To Package No.,Bis Paket Nr., @@ -8290,7 +8283,6 @@ Under AMC,Innerhalb des jährlichen Wartungsvertrags, Out of AMC,Außerhalb des jährlichen Wartungsvertrags, Warranty Period (Days),Garantiefrist (Tage), Serial No Details,Details zur Seriennummer, -MAT-STE-.YYYY.-,MAT-STE-.JJJJ.-, Stock Entry Type,Bestandsbuchungsart, Stock Entry (Outward GIT),Bestandsbuchung (Outward GIT), Material Consumption for Manufacture,Materialverbrauch für die Herstellung, @@ -8336,7 +8328,6 @@ Stock Queue (FIFO),Lagerverfahren (FIFO), Is Cancelled,Ist storniert, Stock Reconciliation,Bestandsabgleich, This tool helps you to update or fix the quantity and valuation of stock in the system. It is typically used to synchronise the system values and what actually exists in your warehouses.,"Dieses Werkzeug hilft Ihnen dabei, die Menge und die Bewertung von Bestand im System zu aktualisieren oder zu ändern. Es wird in der Regel verwendet, um die Systemwerte und den aktuellen Bestand Ihrer Lager zu synchronisieren.", -MAT-RECO-.YYYY.-,MAT-RECO-.YYYY.-, Reconciliation JSON,Abgleich JSON (JavaScript Object Notation), Stock Reconciliation Item,Bestandsabgleich-Artikel, Before reconciliation,Vor Ausgleich, @@ -8796,8 +8787,7 @@ Availed ITC State/UT Tax,Verfügbare ITC State / UT Tax, Availed ITC Cess,ITC Cess verfügbar, Is Nil Rated or Exempted,Ist gleich Null oder ausgenommen, Is Non GST,Ist nicht GST, -ACC-SINV-RET-.YYYY.-,ACC-SINV-RET-.YYYY.-, -E-Way Bill No.,E-Way Bill No., +E-Way Bill No.,E-Way Bill Nr., Is Consolidated,Ist konsolidiert, Billing Address GSTIN,Rechnungsadresse GSTIN, Customer GSTIN,Kunde GSTIN, @@ -9216,7 +9206,7 @@ Id,Ich würde, Time Required (In Mins),Erforderliche Zeit (in Minuten), From Posting Date,Ab dem Buchungsdatum, To Posting Date,Zum Buchungsdatum, -No records found,Keine Aufzeichnungen gefunden, +No records found,Keine Einträge gefunden, Customer/Lead Name,Name des Kunden / Lead, Unmarked Days,Nicht markierte Tage, Jan,Jan., @@ -9275,7 +9265,7 @@ Delay (in Days),Verzögerung (in Tagen), Group by Sales Order,Nach Auftrag gruppieren, Sales Value,Verkaufswert, Stock Qty vs Serial No Count,Lagermenge vs Seriennummer, -Serial No Count,Seriennummer nicht gezählt, +Serial No Count,Seriennummern gezählt, Work Order Summary,Arbeitsauftragsübersicht, Produce Qty,Menge produzieren, Lead Time (in mins),Vorlaufzeit (in Minuten), @@ -9569,7 +9559,7 @@ Row #{}: Selling rate for item {} is lower than its {}. Selling {} should be atl You can alternatively disable selling price validation in {} to bypass this validation.,"Alternativ können Sie die Validierung des Verkaufspreises in {} deaktivieren, um diese Validierung zu umgehen.", Invalid Selling Price,Ungültiger Verkaufspreis, Address needs to be linked to a Company. Please add a row for Company in the Links table.,Die Adresse muss mit einem Unternehmen verknüpft sein. Bitte fügen Sie eine Zeile für Firma in die Tabelle Links ein., -Company Not Linked,Firma nicht verbunden, +Company Not Linked,Firma nicht verknüpft, Import Chart of Accounts from CSV / Excel files,Kontenplan aus CSV / Excel-Dateien importieren, Completed Qty cannot be greater than 'Qty to Manufacture',Die abgeschlossene Menge darf nicht größer sein als die Menge bis zur Herstellung., "Row {0}: For Supplier {1}, Email Address is Required to send an email","Zeile {0}: Für Lieferant {1} ist eine E-Mail-Adresse erforderlich, um eine E-Mail zu senden", @@ -9656,7 +9646,7 @@ Hide Customer's Tax ID from Sales Transactions,Steuer-ID des Kunden vor Verkaufs Action If Quality Inspection Is Not Submitted,Maßnahme Wenn keine Qualitätsprüfung eingereicht wird, Auto Insert Price List Rate If Missing,"Preisliste automatisch einfügen, falls fehlt", Automatically Set Serial Nos Based on FIFO,Seriennummern basierend auf FIFO automatisch einstellen, -Set Qty in Transactions Based on Serial No Input,Stellen Sie die Menge in Transaktionen basierend auf Seriennummer ohne Eingabe ein, +Set Qty in Transactions Based on Serial No Input,Setze die Anzahl in der Transaktion basierend auf den Seriennummern, Raise Material Request When Stock Reaches Re-order Level,"Erhöhen Sie die Materialanforderung, wenn der Lagerbestand die Nachbestellmenge erreicht", Notify by Email on Creation of Automatic Material Request,Benachrichtigen Sie per E-Mail über die Erstellung einer automatischen Materialanforderung, Allow Material Transfer from Delivery Note to Sales Invoice,Materialübertragung vom Lieferschein zur Ausgangsrechnung zulassen, @@ -9765,7 +9755,7 @@ Open Form View,Öffnen Sie die Formularansicht, POS invoice {0} created succesfully,POS-Rechnung {0} erfolgreich erstellt, Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.,Lagermenge nicht ausreichend für Artikelcode: {0} unter Lager {1}. Verfügbare Menge {2}., Serial No: {0} has already been transacted into another POS Invoice.,Seriennummer: {0} wurde bereits in eine andere POS-Rechnung übertragen., -Balance Serial No,Balance Seriennr, +Balance Serial No,Stand Seriennummern, Warehouse: {0} does not belong to {1},Lager: {0} gehört nicht zu {1}, Please select batches for batched item {0},Bitte wählen Sie Chargen für Chargenartikel {0} aus, Please select quantity on row {0},Bitte wählen Sie die Menge in Zeile {0}, From 7737b9061fe84841a35141dfd9e79592b09e1515 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 7 Jun 2023 22:36:31 +0530 Subject: [PATCH 05/24] fix: Project in item-wise sales register (#35596) fix: Project in item-wise sales register (#35596) (cherry picked from commit f732cac6780e0f9c9aead4df381a0bbe5774354c) Co-authored-by: Deepesh Garg --- .../item_wise_sales_register/item_wise_sales_register.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index dd9c0736128..0ebe13f4f32 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -399,8 +399,9 @@ def get_items(filters, additional_query_columns, additional_conditions=None): `tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to, `tabSales Invoice`.unrealized_profit_loss_account, `tabSales Invoice`.is_internal_customer, - `tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks, + `tabSales Invoice`.customer, `tabSales Invoice`.remarks, `tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total, + `tabSales Invoice Item`.project, `tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description, `tabSales Invoice Item`.`item_name`, `tabSales Invoice Item`.`item_group`, `tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note, From 29e079d5d6a8d2ae61f93df5a5b5147143f7004b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 8 Jun 2023 19:59:22 +0530 Subject: [PATCH 06/24] refactor: use delete_contact_and_address (#34497) refactor: use delete_contact_and_address (#34497) Co-authored-by: Deepesh Garg (cherry picked from commit 0dde4d4c69f673565696711834f85853dd8673b8) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- erpnext/crm/doctype/lead/lead.py | 31 +++++------------------- erpnext/crm/doctype/prospect/prospect.py | 28 ++++----------------- 2 files changed, 11 insertions(+), 48 deletions(-) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index 6443928bf66..08ea4b06e7c 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -3,7 +3,10 @@ import frappe from frappe import _ -from frappe.contacts.address_and_contact import load_address_and_contact +from frappe.contacts.address_and_contact import ( + delete_contact_and_address, + load_address_and_contact, +) from frappe.email.inbox import link_communication_to_document from frappe.model.mapper import get_mapped_doc from frappe.utils import comma_and, get_link_to_form, has_gravatar, validate_email_address @@ -43,9 +46,8 @@ class Lead(SellingController, CRMNote): self.update_prospect() def on_trash(self): - frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name) - - self.unlink_dynamic_links() + frappe.db.set_value("Issue", {"lead": self.name}, "lead", None) + delete_contact_and_address(self.doctype, self.name) self.remove_link_from_prospect() def set_full_name(self): @@ -122,27 +124,6 @@ class Lead(SellingController, CRMNote): ) lead_row.db_update() - def unlink_dynamic_links(self): - links = frappe.get_all( - "Dynamic Link", - filters={"link_doctype": self.doctype, "link_name": self.name}, - fields=["parent", "parenttype"], - ) - - for link in links: - linked_doc = frappe.get_doc(link["parenttype"], link["parent"]) - - if len(linked_doc.get("links")) == 1: - linked_doc.delete(ignore_permissions=True) - else: - to_remove = None - for d in linked_doc.get("links"): - if d.link_doctype == self.doctype and d.link_name == self.name: - to_remove = d - if to_remove: - linked_doc.remove(to_remove) - linked_doc.save(ignore_permissions=True) - def remove_link_from_prospect(self): prospects = self.get_linked_prospects() diff --git a/erpnext/crm/doctype/prospect/prospect.py b/erpnext/crm/doctype/prospect/prospect.py index fbb115883f9..8b66a83f2ae 100644 --- a/erpnext/crm/doctype/prospect/prospect.py +++ b/erpnext/crm/doctype/prospect/prospect.py @@ -2,7 +2,10 @@ # For license information, please see license.txt import frappe -from frappe.contacts.address_and_contact import load_address_and_contact +from frappe.contacts.address_and_contact import ( + delete_contact_and_address, + load_address_and_contact, +) from frappe.model.mapper import get_mapped_doc from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_open_events @@ -16,7 +19,7 @@ class Prospect(CRMNote): self.link_with_lead_contact_and_address() def on_trash(self): - self.unlink_dynamic_links() + delete_contact_and_address(self.doctype, self.name) def after_insert(self): carry_forward_communication_and_comments = frappe.db.get_single_value( @@ -54,27 +57,6 @@ class Prospect(CRMNote): linked_doc.append("links", {"link_doctype": self.doctype, "link_name": self.name}) linked_doc.save(ignore_permissions=True) - def unlink_dynamic_links(self): - links = frappe.get_all( - "Dynamic Link", - filters={"link_doctype": self.doctype, "link_name": self.name}, - fields=["parent", "parenttype"], - ) - - for link in links: - linked_doc = frappe.get_doc(link["parenttype"], link["parent"]) - - if len(linked_doc.get("links")) == 1: - linked_doc.delete(ignore_permissions=True) - else: - to_remove = None - for d in linked_doc.get("links"): - if d.link_doctype == self.doctype and d.link_name == self.name: - to_remove = d - if to_remove: - linked_doc.remove(to_remove) - linked_doc.save(ignore_permissions=True) - @frappe.whitelist() def make_customer(source_name, target_doc=None): From 059141df52b888a6ad1ab8346d605b0c1b9b1fdc Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 8 Jun 2023 22:04:13 +0530 Subject: [PATCH 07/24] refactor: get default contact or address (#35248) refactor: get default contact or address (#35248) * refactor: get_party_shipping_address * refactor: get_default_contact * chore: adding docstrings * fix: keep original order * fix: use get_all instead of get_list --------- Co-authored-by: ruthra kumar (cherry picked from commit b91bb17779eb04a5d14bbc065ec2ed72d97baa5a) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- erpnext/accounts/party.py | 80 ++++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 25ea903dfec..7583a60d63e 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -2,6 +2,8 @@ # License: GNU General Public License v3. See license.txt +from typing import Optional + import frappe from frappe import _, msgprint, scrub from frappe.contacts.doctype.address.address import ( @@ -850,7 +852,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None): return company_wise_info -def get_party_shipping_address(doctype, name): +def get_party_shipping_address(doctype: str, name: str) -> Optional[str]: """ Returns an Address name (best guess) for the given doctype and name for which `address_type == 'Shipping'` is true. and/or `is_shipping_address = 1`. @@ -861,22 +863,23 @@ def get_party_shipping_address(doctype, name): :param name: Party name :return: String """ - out = frappe.db.sql( - "SELECT dl.parent " - "from `tabDynamic Link` dl join `tabAddress` ta on dl.parent=ta.name " - "where " - "dl.link_doctype=%s " - "and dl.link_name=%s " - "and dl.parenttype='Address' " - "and ifnull(ta.disabled, 0) = 0 and" - "(ta.address_type='Shipping' or ta.is_shipping_address=1) " - "order by ta.is_shipping_address desc, ta.address_type desc limit 1", - (doctype, name), + shipping_addresses = frappe.get_all( + "Address", + filters=[ + ["Dynamic Link", "link_doctype", "=", doctype], + ["Dynamic Link", "link_name", "=", name], + ["disabled", "=", 0], + ], + or_filters=[ + ["is_shipping_address", "=", 1], + ["address_type", "=", "Shipping"], + ], + pluck="name", + limit=1, + order_by="is_shipping_address DESC", ) - if out: - return out[0][0] - else: - return "" + + return shipping_addresses[0] if shipping_addresses else None def get_partywise_advanced_payment_amount( @@ -910,31 +913,32 @@ def get_partywise_advanced_payment_amount( return frappe._dict(data) -def get_default_contact(doctype, name): +def get_default_contact(doctype: str, name: str) -> Optional[str]: """ - Returns default contact for the given doctype and name. - Can be ordered by `contact_type` to either is_primary_contact or is_billing_contact. + Returns contact name only if there is a primary contact for given doctype and name. + + Else returns None + + :param doctype: Party Doctype + :param name: Party name + :return: String """ - out = frappe.db.sql( - """ - SELECT dl.parent, c.is_primary_contact, c.is_billing_contact - FROM `tabDynamic Link` dl - INNER JOIN `tabContact` c ON c.name = dl.parent - WHERE - dl.link_doctype=%s AND - dl.link_name=%s AND - dl.parenttype = 'Contact' - ORDER BY is_primary_contact DESC, is_billing_contact DESC - """, - (doctype, name), + contacts = frappe.get_all( + "Contact", + filters=[ + ["Dynamic Link", "link_doctype", "=", doctype], + ["Dynamic Link", "link_name", "=", name], + ], + or_filters=[ + ["is_primary_contact", "=", 1], + ["is_billing_contact", "=", 1], + ], + pluck="name", + limit=1, + order_by="is_primary_contact DESC, is_billing_contact DESC", ) - if out: - try: - return out[0][0] - except Exception: - return None - else: - return None + + return contacts[0] if contacts else None def add_party_account(party_type, party, company, account): From feb5d0089b48ed48748c8383a7c4f324ef822bb9 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Thu, 8 Jun 2023 23:16:28 +0530 Subject: [PATCH 08/24] fix: calculate wdv depr schedule properly for existing assets [v14] (#35613) * fix: calculate wdv depr schedule properly for existing assets * fix: calculate wdv depr schedule properly for existing assets properly --- erpnext/assets/doctype/asset/asset.py | 27 +++++++++++++++----- erpnext/assets/doctype/asset/depreciation.py | 17 +++++++++++- erpnext/assets/doctype/asset/test_asset.py | 4 +-- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index ae77b9b94ce..02260c6da3a 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -26,6 +26,7 @@ from erpnext.accounts.general_ledger import make_reverse_gl_entries from erpnext.assets.doctype.asset.depreciation import ( get_depreciation_accounts, get_disposal_account_and_cost_center, + is_first_day_of_the_month, is_last_day_of_the_month, ) from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account @@ -359,8 +360,14 @@ class Asset(AccountsController): break # For first row - if n == 0 and has_pro_rata and not self.opening_accumulated_depreciation: - from_date = add_days(self.available_for_use_date, -1) + if ( + n == 0 + and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata) + and not self.opening_accumulated_depreciation + ): + from_date = add_days( + self.available_for_use_date, -1 + ) # needed to calc depr amount for available_for_use_date too depreciation_amount, days, months = self.get_pro_rata_amt( finance_book, depreciation_amount, @@ -369,10 +376,18 @@ class Asset(AccountsController): has_wdv_or_dd_non_yearly_pro_rata, ) elif n == 0 and has_wdv_or_dd_non_yearly_pro_rata and self.opening_accumulated_depreciation: - from_date = add_months( - getdate(self.available_for_use_date), - (self.number_of_depreciations_booked * finance_book.frequency_of_depreciation), - ) + if not is_first_day_of_the_month(getdate(self.available_for_use_date)): + from_date = get_last_day( + add_months( + getdate(self.available_for_use_date), + ((self.number_of_depreciations_booked - 1) * finance_book.frequency_of_depreciation), + ) + ) + else: + from_date = add_months( + getdate(add_days(self.available_for_use_date, -1)), + (self.number_of_depreciations_booked * finance_book.frequency_of_depreciation), + ) depreciation_amount, days, months = self.get_pro_rata_amt( finance_book, depreciation_amount, diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 163752b65eb..0c8d5f527fd 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -4,7 +4,16 @@ import frappe from frappe import _ -from frappe.utils import add_months, cint, flt, get_last_day, getdate, nowdate, today +from frappe.utils import ( + add_months, + cint, + flt, + get_first_day, + get_last_day, + getdate, + nowdate, + today, +) from frappe.utils.data import get_link_to_form from frappe.utils.user import get_users_with_role @@ -591,3 +600,9 @@ def is_last_day_of_the_month(date): last_day_of_the_month = get_last_day(date) return getdate(last_day_of_the_month) == getdate(date) + + +def is_first_day_of_the_month(date): + first_day_of_the_month = get_first_day(date) + + return getdate(first_day_of_the_month) == getdate(date) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 1c0972f2374..fea6ed3d2bd 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -766,14 +766,14 @@ class TestDepreciationMethods(AssetSetup): number_of_depreciations_booked=1, opening_accumulated_depreciation=50000, expected_value_after_useful_life=10000, - depreciation_start_date="2030-12-31", + depreciation_start_date="2031-12-31", total_number_of_depreciations=3, frequency_of_depreciation=12, ) self.assertEqual(asset.status, "Draft") - expected_schedules = [["2030-12-31", 33333.50, 83333.50], ["2031-12-31", 6666.50, 90000.0]] + expected_schedules = [["2031-12-31", 33333.50, 83333.50], ["2032-12-31", 6666.50, 90000.0]] schedules = [ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] From 32e5bbbb46ebe0872363f87d430d0fa9559a00dd Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 7 Jun 2023 09:30:23 +0530 Subject: [PATCH 09/24] fix: `TypeError` in Closing Stock Balance (cherry picked from commit 446253ff399151709b3e4db2d186001b17b247b6) --- .../doctype/closing_stock_balance/closing_stock_balance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py index a7963726ae3..295d979b835 100644 --- a/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py +++ b/erpnext/stock/doctype/closing_stock_balance/closing_stock_balance.py @@ -51,7 +51,7 @@ class ClosingStockBalance(Document): for fieldname in ["warehouse", "item_code", "item_group", "warehouse_type"]: if self.get(fieldname): - query = query.where(table.get(fieldname) == self.get(fieldname)) + query = query.where(table[fieldname] == self.get(fieldname)) query = query.run(as_dict=True) From 2cf871c21e6081ccb8902669819a4eaff49aceb6 Mon Sep 17 00:00:00 2001 From: Trusted Computer <75872475+trustedcomputer@users.noreply.github.com> Date: Sun, 11 Jun 2023 07:05:24 -0700 Subject: [PATCH 10/24] fix: CSS not applied to product title (#35630) --- erpnext/e_commerce/product_ui/list.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/e_commerce/product_ui/list.js b/erpnext/e_commerce/product_ui/list.js index 894a7cb3d87..c8fd7672c8e 100644 --- a/erpnext/e_commerce/product_ui/list.js +++ b/erpnext/e_commerce/product_ui/list.js @@ -78,9 +78,10 @@ erpnext.ProductList = class { let title_html = `
`; title_html += ` `; @@ -201,4 +202,4 @@ erpnext.ProductList = class { } } -}; \ No newline at end of file +}; From 043815e745807b6a99b1570e2aa1b4a84f7b52b1 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 12 Jun 2023 15:35:48 +0530 Subject: [PATCH 11/24] fix: Make difference entry button not working (#35622) fix: Make difference entry button not working (#35622) (cherry picked from commit 2f24546b21cea8834ceb92d7cc72314fa2750124) Co-authored-by: Deepesh Garg --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 92c37bbe5e5..b275cada953 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -940,6 +940,7 @@ class JournalEntry(AccountsController): blank_row.debit_in_account_currency = abs(diff) blank_row.debit = abs(diff) + self.set_total_debit_credit() self.validate_total_debit_and_credit() @frappe.whitelist() From 81ef2babe97fcaebd0765d48d1219a741162cf00 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 12 Jun 2023 18:06:26 +0530 Subject: [PATCH 12/24] fix: Payment against credit notes will be considered as payment against parent invoice in Accounts Receivable/Payable report (#35642) fix: Payment against credit notes will be considered as payment against parent invoice in Accounts Receivable/Payable report (#35642) * fix: payment against credit note should be linked to parent invoice * test: AR/AP report for payment against cr note scenario * fix: cr_note shows up as outstanding invoice Payment made against cr_note causes it be reported as outstanding invoice (cherry picked from commit 42f4f80e0cc4fc6a52f4bce99234b8f1b8ddc395) Co-authored-by: ruthra kumar --- .../payment_reconciliation.py | 40 +++++++----- .../accounts_receivable.py | 12 +++- .../test_accounts_receivable.py | 65 ++++++++++++++++++- 3 files changed, 100 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index cc2b9420cc2..081fe70354d 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -6,7 +6,6 @@ import frappe from frappe import _, msgprint, qb from frappe.model.document import Document from frappe.query_builder.custom import ConstantColumn -from frappe.query_builder.functions import IfNull from frappe.utils import flt, get_link_to_form, getdate, nowdate, today import erpnext @@ -127,12 +126,29 @@ class PaymentReconciliation(Document): return list(journal_entries) + def get_return_invoices(self): + voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" + doc = qb.DocType(voucher_type) + self.return_invoices = ( + qb.from_(doc) + .select( + ConstantColumn(voucher_type).as_("voucher_type"), + doc.name.as_("voucher_no"), + doc.return_against, + ) + .where( + (doc.docstatus == 1) + & (doc[frappe.scrub(self.party_type)] == self.party) + & (doc.is_return == 1) + ) + .run(as_dict=True) + ) + def get_dr_or_cr_notes(self): self.build_qb_filter_conditions(get_return_invoices=True) ple = qb.DocType("Payment Ledger Entry") - voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" if erpnext.get_party_account_type(self.party_type) == "Receivable": self.common_filter_conditions.append(ple.account_type == "Receivable") @@ -140,19 +156,10 @@ class PaymentReconciliation(Document): self.common_filter_conditions.append(ple.account_type == "Payable") self.common_filter_conditions.append(ple.account == self.receivable_payable_account) - # get return invoices - doc = qb.DocType(voucher_type) - return_invoices = ( - qb.from_(doc) - .select(ConstantColumn(voucher_type).as_("voucher_type"), doc.name.as_("voucher_no")) - .where( - (doc.docstatus == 1) - & (doc[frappe.scrub(self.party_type)] == self.party) - & (doc.is_return == 1) - & (IfNull(doc.return_against, "") == "") - ) - .run(as_dict=True) - ) + self.get_return_invoices() + return_invoices = [ + x for x in self.return_invoices if x.return_against == None or x.return_against == "" + ] outstanding_dr_or_cr = [] if return_invoices: @@ -204,6 +211,9 @@ class PaymentReconciliation(Document): accounting_dimensions=self.accounting_dimension_filter_conditions, ) + cr_dr_notes = [x.voucher_no for x in self.return_invoices] + non_reconciled_invoices = [x for x in non_reconciled_invoices if x.voucher_no not in cr_dr_notes] + if self.invoice_limit: non_reconciled_invoices = non_reconciled_invoices[: self.invoice_limit] diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 11de9a098dc..30f7fb38c5f 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -181,6 +181,16 @@ class ReceivablePayableReport(object): return key = (ple.against_voucher_type, ple.against_voucher_no, ple.party) + + # If payment is made against credit note + # and credit note is made against a Sales Invoice + # then consider the payment against original sales invoice. + if ple.against_voucher_type in ("Sales Invoice", "Purchase Invoice"): + if ple.against_voucher_no in self.return_entries: + return_against = self.return_entries.get(ple.against_voucher_no) + if return_against: + key = (ple.against_voucher_type, return_against, ple.party) + row = self.voucher_balance.get(key) if not row: @@ -610,7 +620,7 @@ class ReceivablePayableReport(object): def get_return_entries(self): doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" - filters = {"is_return": 1, "docstatus": 1} + filters = {"is_return": 1, "docstatus": 1, "company": self.filters.company} party_field = scrub(self.filters.party_type) if self.filters.get(party_field): filters.update({party_field: self.filters.get(party_field)}) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index afd02a006e6..6f1889b34e1 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -210,6 +210,67 @@ class TestAccountsReceivable(FrappeTestCase): ], ) + def test_payment_against_credit_note(self): + """ + Payment against credit/debit note should be considered against the parent invoice + """ + company = "_Test Company 2" + customer = "_Test Customer 2" + + si1 = make_sales_invoice() + + pe = get_payment_entry("Sales Invoice", si1.name, bank_account="Cash - _TC2") + pe.paid_from = "Debtors - _TC2" + pe.insert() + pe.submit() + + cr_note = make_credit_note(si1.name) + + si2 = make_sales_invoice() + + # manually link cr_note with si2 using journal entry + je = frappe.new_doc("Journal Entry") + je.company = company + je.voucher_type = "Credit Note" + je.posting_date = today() + + debit_account = "Debtors - _TC2" + debit_entry = { + "account": debit_account, + "party_type": "Customer", + "party": customer, + "debit": 100, + "debit_in_account_currency": 100, + "reference_type": cr_note.doctype, + "reference_name": cr_note.name, + "cost_center": "Main - _TC2", + } + credit_entry = { + "account": debit_account, + "party_type": "Customer", + "party": customer, + "credit": 100, + "credit_in_account_currency": 100, + "reference_type": si2.doctype, + "reference_name": si2.name, + "cost_center": "Main - _TC2", + } + + je.append("accounts", debit_entry) + je.append("accounts", credit_entry) + je = je.save().submit() + + filters = { + "company": company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + report = execute(filters) + self.assertEqual(report[1], []) + def make_sales_invoice(no_payment_schedule=False, do_not_submit=False): frappe.set_user("Administrator") @@ -256,7 +317,7 @@ def make_payment(docname): def make_credit_note(docname): - create_sales_invoice( + credit_note = create_sales_invoice( company="_Test Company 2", customer="_Test Customer 2", currency="EUR", @@ -269,3 +330,5 @@ def make_credit_note(docname): is_return=1, return_against=docname, ) + + return credit_note From 79483cc90eb71fa82d645b2cf54d9731502cf6c7 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Mon, 12 Jun 2023 18:34:13 +0530 Subject: [PATCH 13/24] fix: don't set default payment amount in case of invoice return (#35645) --- erpnext/public/js/controllers/taxes_and_totals.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index fd961c4aaae..6f4e602abb6 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -805,11 +805,13 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { ); } - this.frm.doc.payments.find(pay => { - if (pay.default) { - pay.amount = total_amount_to_pay; - } - }); + if(!this.frm.doc.is_return){ + this.frm.doc.payments.find(payment => { + if (payment.default) { + payment.amount = total_amount_to_pay; + } + }); + } this.frm.refresh_fields(); } From 8b617fb75e374f199a3d942bd03a4344f99bbbc0 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 12 Jun 2023 18:28:16 +0530 Subject: [PATCH 14/24] fix: Stock Reconciliation document update while reposting (cherry picked from commit db159dd11f66862f35123041d2195fe6490a243f) --- erpnext/stock/stock_ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 272195e2a68..be2beb1bfb5 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -803,7 +803,7 @@ class update_entries_after(object): for item in sr.items: # Skip for Serial and Batch Items - if item.serial_no or item.batch_no: + if item.name != sle.voucher_detail_no or item.serial_no or item.batch_no: continue previous_sle = get_previous_sle( From 6a21d617cebf46420ab166c817a98fc0dc97deec Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 9 Jun 2023 20:33:46 +0530 Subject: [PATCH 15/24] fix: added process loss in job card (cherry picked from commit e9a6191af97a37acc6ab90f91677d072fa3c94b1) # Conflicts: # erpnext/manufacturing/doctype/job_card/job_card.json --- .../doctype/job_card/job_card.js | 12 +++- .../doctype/job_card/job_card.json | 20 ++++++- .../doctype/job_card/job_card.py | 57 +++++++++++++++---- .../doctype/work_order/work_order.js | 15 +++-- .../doctype/work_order/work_order.json | 5 +- .../doctype/work_order/work_order.py | 14 +++-- .../work_order_operation.json | 20 ++++++- .../stock/doctype/stock_entry/stock_entry.js | 15 +++++ .../doctype/stock_entry/stock_entry.json | 15 +++-- .../stock/doctype/stock_entry/stock_entry.py | 31 ++++++++-- 10 files changed, 166 insertions(+), 38 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 5305db318b1..4a46d577445 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -83,7 +83,7 @@ frappe.ui.form.on('Job Card', { // and if stock mvt for WIP is required if (frm.doc.work_order) { frappe.db.get_value('Work Order', frm.doc.work_order, ['skip_transfer', 'status'], (result) => { - if (result.skip_transfer === 1 || result.status == 'In Process' || frm.doc.transferred_qty > 0) { + if (result.skip_transfer === 1 || result.status == 'In Process' || frm.doc.transferred_qty > 0 || !frm.doc.items.length) { frm.trigger("prepare_timer_buttons"); } }); @@ -411,6 +411,16 @@ frappe.ui.form.on('Job Card', { } }); + if (frm.doc.total_completed_qty && frm.doc.for_quantity > frm.doc.total_completed_qty) { + let flt_precision = precision('for_quantity', frm.doc); + let process_loss_qty = ( + flt(frm.doc.for_quantity, flt_precision) + - flt(frm.doc.total_completed_qty, flt_precision) + ); + + frm.set_value('process_loss_qty', process_loss_qty); + } + refresh_field("total_completed_qty"); } }); diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 316e586b7a2..3cd1f54b69e 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -38,6 +38,7 @@ "time_logs", "section_break_13", "total_completed_qty", + "process_loss_qty", "column_break_15", "total_time_in_mins", "section_break_8", @@ -435,11 +436,28 @@ "fieldname": "expected_end_date", "fieldtype": "Datetime", "label": "Expected End Date" +<<<<<<< HEAD +======= + }, + { + "fieldname": "serial_and_batch_bundle", + "fieldtype": "Link", + "label": "Serial and Batch Bundle", + "no_copy": 1, + "options": "Serial and Batch Bundle", + "print_hide": 1 + }, + { + "fieldname": "process_loss_qty", + "fieldtype": "Float", + "label": "Process Loss Qty", + "read_only": 1 +>>>>>>> e9a6191af9 (fix: added process loss in job card) } ], "is_submittable": 1, "links": [], - "modified": "2023-05-23 09:56:43.826602", + "modified": "2023-06-09 12:04:55.534264", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index fcaa3fd276f..496cbfd0a6b 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -161,7 +161,7 @@ class JobCard(Document): self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty")) for row in self.sub_operations: - self.total_completed_qty += row.completed_qty + self.c += row.completed_qty def get_overlap_for(self, args, check_next_available_slot=False): production_capacity = 1 @@ -451,6 +451,9 @@ class JobCard(Document): }, ) + def before_save(self): + self.set_process_loss() + def on_submit(self): self.validate_transfer_qty() self.validate_job_card() @@ -487,19 +490,35 @@ class JobCard(Document): ) ) - if self.for_quantity and self.total_completed_qty != self.for_quantity: + precision = self.precision("total_completed_qty") + total_completed_qty = flt( + flt(self.total_completed_qty, precision) + flt(self.process_loss_qty, precision) + ) + + if self.for_quantity and flt(total_completed_qty, precision) != flt( + self.for_quantity, precision + ): total_completed_qty = bold(_("Total Completed Qty")) qty_to_manufacture = bold(_("Qty to Manufacture")) frappe.throw( _("The {0} ({1}) must be equal to {2} ({3})").format( total_completed_qty, - bold(self.total_completed_qty), + bold(flt(total_completed_qty, precision)), qty_to_manufacture, bold(self.for_quantity), ) ) + def set_process_loss(self): + precision = self.precision("total_completed_qty") + + self.process_loss_qty = 0.0 + if self.total_completed_qty and self.for_quantity > self.total_completed_qty: + self.process_loss_qty = flt(self.for_quantity, precision) - flt( + self.total_completed_qty, precision + ) + def update_work_order(self): if not self.work_order: return @@ -511,7 +530,7 @@ class JobCard(Document): ): return - for_quantity, time_in_mins = 0, 0 + for_quantity, time_in_mins, process_loss_qty = 0, 0, 0 from_time_list, to_time_list = [], [] field = "operation_id" @@ -519,6 +538,7 @@ class JobCard(Document): if data and len(data) > 0: for_quantity = flt(data[0].completed_qty) time_in_mins = flt(data[0].time_in_mins) + process_loss_qty = flt(data[0].process_loss_qty) wo = frappe.get_doc("Work Order", self.work_order) @@ -526,8 +546,8 @@ class JobCard(Document): self.update_corrective_in_work_order(wo) elif self.operation_id: - self.validate_produced_quantity(for_quantity, wo) - self.update_work_order_data(for_quantity, time_in_mins, wo) + self.validate_produced_quantity(for_quantity, process_loss_qty, wo) + self.update_work_order_data(for_quantity, process_loss_qty, time_in_mins, wo) def update_corrective_in_work_order(self, wo): wo.corrective_operation_cost = 0.0 @@ -542,11 +562,11 @@ class JobCard(Document): wo.flags.ignore_validate_update_after_submit = True wo.save() - def validate_produced_quantity(self, for_quantity, wo): + def validate_produced_quantity(self, for_quantity, process_loss_qty, wo): if self.docstatus < 2: return - if wo.produced_qty > for_quantity: + if wo.produced_qty > for_quantity + process_loss_qty: first_part_msg = _( "The {0} {1} is used to calculate the valuation cost for the finished good {2}." ).format( @@ -561,7 +581,7 @@ class JobCard(Document): _("{0} {1}").format(first_part_msg, second_part_msg), JobCardCancelError, title=_("Error") ) - def update_work_order_data(self, for_quantity, time_in_mins, wo): + def update_work_order_data(self, for_quantity, process_loss_qty, time_in_mins, wo): workstation_hour_rate = frappe.get_value("Workstation", self.workstation, "hour_rate") jc = frappe.qb.DocType("Job Card") jctl = frappe.qb.DocType("Job Card Time Log") @@ -582,6 +602,7 @@ class JobCard(Document): for data in wo.operations: if data.get("name") == self.operation_id: data.completed_qty = for_quantity + data.process_loss_qty = process_loss_qty data.actual_operation_time = time_in_mins data.actual_start_time = time_data[0].start_time if time_data else None data.actual_end_time = time_data[0].end_time if time_data else None @@ -599,7 +620,11 @@ class JobCard(Document): def get_current_operation_data(self): return frappe.get_all( "Job Card", - fields=["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"], + fields=[ + "sum(total_time_in_mins) as time_in_mins", + "sum(total_completed_qty) as completed_qty", + "sum(process_loss_qty) as process_loss_qty", + ], filters={ "docstatus": 1, "work_order": self.work_order, @@ -777,7 +802,7 @@ class JobCard(Document): data = frappe.get_all( "Work Order Operation", - fields=["operation", "status", "completed_qty"], + fields=["operation", "status", "completed_qty", "sequence_id"], filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ("<", self.sequence_id)}, order_by="sequence_id, idx", ) @@ -795,6 +820,16 @@ class JobCard(Document): OperationSequenceError, ) + if row.completed_qty < current_operation_qty: + msg = f"""The completed quantity {bold(current_operation_qty)} + of an operation {bold(self.operation)} cannot be greater + than the completed quantity {bold(row.completed_qty)} + of a previous operation + {bold(row.operation)}. + """ + + frappe.throw(_(msg)) + def validate_work_order(self): if self.is_work_order_closed(): frappe.throw(_("You can't make any changes to Job Card since Work Order is closed.")) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index d0c9966f8ba..c1a078d65e0 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -139,7 +139,7 @@ frappe.ui.form.on("Work Order", { } if (frm.doc.status != "Closed") { - if (frm.doc.docstatus === 1 + if (frm.doc.docstatus === 1 && frm.doc.status !== "Completed" && frm.doc.operations && frm.doc.operations.length) { const not_completed = frm.doc.operations.filter(d => { @@ -256,6 +256,12 @@ frappe.ui.form.on("Work Order", { label: __('Batch Size'), read_only: 1 }, + { + fieldtype: 'Int', + fieldname: 'sequence_id', + label: __('Sequence Id'), + read_only: 1 + }, ], data: operations_data, in_place_edit: true, @@ -280,8 +286,8 @@ frappe.ui.form.on("Work Order", { var pending_qty = 0; frm.doc.operations.forEach(data => { - if(data.completed_qty != frm.doc.qty) { - pending_qty = frm.doc.qty - flt(data.completed_qty); + if(data.completed_qty + data.process_loss_qty != frm.doc.qty) { + pending_qty = frm.doc.qty - flt(data.completed_qty) - flt(data.process_loss_qty); if (pending_qty) { dialog.fields_dict.operations.df.data.push({ @@ -290,7 +296,8 @@ frappe.ui.form.on("Work Order", { 'workstation': data.workstation, 'batch_size': data.batch_size, 'qty': pending_qty, - 'pending_qty': pending_qty + 'pending_qty': pending_qty, + 'sequence_id': data.sequence_id }); } } diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index aa9049801cc..38e72533ba0 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -47,8 +47,8 @@ "required_items_section", "materials_and_operations_tab", "operations_section", - "operations", "transfer_material_against", + "operations", "time", "planned_start_date", "planned_end_date", @@ -331,7 +331,6 @@ "label": "Expected Delivery Date" }, { - "collapsible": 1, "fieldname": "operations_section", "fieldtype": "Section Break", "label": "Operations", @@ -599,7 +598,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2023-04-06 12:35:12.149827", + "modified": "2023-06-09 13:20:09.154362", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 75845226a65..19e081acf73 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -249,7 +249,9 @@ class WorkOrder(Document): status = "Not Started" if flt(self.material_transferred_for_manufacturing) > 0: status = "In Process" - if flt(self.produced_qty) >= flt(self.qty): + + total_qty = flt(self.produced_qty) + flt(self.process_loss_qty) + if flt(total_qty) >= flt(self.qty): status = "Completed" else: status = "Cancelled" @@ -736,13 +738,15 @@ class WorkOrder(Document): max_allowed_qty_for_wo = flt(self.qty) + (allowance_percentage / 100 * flt(self.qty)) for d in self.get("operations"): - if not d.completed_qty: + precision = d.precision("completed_qty") + qty = flt(d.completed_qty, precision) + flt(d.process_loss_qty, precision) + if not qty: d.status = "Pending" - elif flt(d.completed_qty) < flt(self.qty): + elif flt(qty) < flt(self.qty): d.status = "Work in Progress" - elif flt(d.completed_qty) == flt(self.qty): + elif flt(qty) == flt(self.qty): d.status = "Completed" - elif flt(d.completed_qty) <= max_allowed_qty_for_wo: + elif flt(qty) <= max_allowed_qty_for_wo: d.status = "Completed" else: frappe.throw(_("Completed Qty cannot be greater than 'Qty to Manufacture'")) diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json index 31b920145e0..de1f67f13fd 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json @@ -2,12 +2,14 @@ "actions": [], "creation": "2014-10-16 14:35:41.950175", "doctype": "DocType", + "editable_grid": 1, "engine": "InnoDB", "field_order": [ "details", "operation", "status", "completed_qty", + "process_loss_qty", "column_break_4", "bom", "workstation_type", @@ -36,6 +38,7 @@ "fieldtype": "Section Break" }, { + "columns": 2, "fieldname": "operation", "fieldtype": "Link", "in_list_view": 1, @@ -46,6 +49,7 @@ "reqd": 1 }, { + "columns": 2, "fieldname": "bom", "fieldtype": "Link", "in_list_view": 1, @@ -62,7 +66,7 @@ "oldfieldtype": "Text" }, { - "columns": 1, + "columns": 2, "description": "Operation completed for how many finished goods?", "fieldname": "completed_qty", "fieldtype": "Float", @@ -80,6 +84,7 @@ "options": "Pending\nWork in Progress\nCompleted" }, { + "columns": 1, "fieldname": "workstation", "fieldtype": "Link", "in_list_view": 1, @@ -115,7 +120,7 @@ "fieldname": "time_in_mins", "fieldtype": "Float", "in_list_view": 1, - "label": "Operation Time", + "label": "Time", "oldfieldname": "time_in_mins", "oldfieldtype": "Currency", "reqd": 1 @@ -203,12 +208,21 @@ "fieldtype": "Link", "label": "Workstation Type", "options": "Workstation Type" + }, + { + "columns": 2, + "fieldname": "process_loss_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Process Loss Qty", + "no_copy": 1, + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-11-09 01:37:56.563068", + "modified": "2023-06-09 14:03:01.612909", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Operation", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index a82c709b60f..bc5ac2addb0 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -677,6 +677,21 @@ frappe.ui.form.on('Stock Entry', { }); } }, + + process_loss_qty(frm) { + if (frm.doc.process_loss_qty) { + frm.doc.process_loss_percentage = flt(frm.doc.process_loss_qty / frm.doc.fg_completed_qty * 100, precision("process_loss_qty", frm.doc)); + refresh_field("process_loss_percentage"); + } + }, + + process_loss_percentage(frm) { + debugger + if (frm.doc.process_loss_percentage) { + frm.doc.process_loss_qty = flt((frm.doc.fg_completed_qty * frm.doc.process_loss_percentage) / 100 , precision("process_loss_qty", frm.doc)); + refresh_field("process_loss_qty"); + } + } }); frappe.ui.form.on('Stock Entry Detail', { diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index bc5533fd2de..9bf679b8955 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -24,6 +24,7 @@ "company", "posting_date", "posting_time", + "column_break_eaoa", "set_posting_time", "inspection_required", "apply_putaway_rule", @@ -640,16 +641,16 @@ }, { "collapsible": 1, + "depends_on": "eval: doc.fg_completed_qty > 0 && in_list([\"Manufacture\", \"Repack\"], doc.purpose)", "fieldname": "section_break_7qsm", "fieldtype": "Section Break", "label": "Process Loss" }, { - "depends_on": "process_loss_percentage", + "depends_on": "eval: doc.fg_completed_qty > 0 && in_list([\"Manufacture\", \"Repack\"], doc.purpose)", "fieldname": "process_loss_qty", "fieldtype": "Float", - "label": "Process Loss Qty", - "read_only": 1 + "label": "Process Loss Qty" }, { "fieldname": "column_break_e92r", @@ -657,8 +658,6 @@ }, { "depends_on": "eval:doc.from_bom && doc.fg_completed_qty", - "fetch_from": "bom_no.process_loss_percentage", - "fetch_if_empty": 1, "fieldname": "process_loss_percentage", "fieldtype": "Percent", "label": "% Process Loss" @@ -667,6 +666,10 @@ "fieldname": "items_section", "fieldtype": "Section Break", "label": "Items" + }, + { + "fieldname": "column_break_eaoa", + "fieldtype": "Column Break" } ], "icon": "fa fa-file-text", @@ -674,7 +677,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-04-06 12:42:56.673180", + "modified": "2023-06-09 15:46:28.418339", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index efadf36199c..e74bc9e2f2b 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -455,13 +455,16 @@ class StockEntry(StockController): if self.purpose == "Manufacture" and self.work_order: for d in self.items: if d.is_finished_item: + if self.process_loss_qty: + d.qty = self.fg_completed_qty - self.process_loss_qty + item_wise_qty.setdefault(d.item_code, []).append(d.qty) precision = frappe.get_precision("Stock Entry Detail", "qty") for item_code, qty_list in item_wise_qty.items(): total = flt(sum(qty_list), precision) - if (self.fg_completed_qty - total) > 0: + if (self.fg_completed_qty - total) > 0 and not self.process_loss_qty: self.process_loss_qty = flt(self.fg_completed_qty - total, precision) self.process_loss_percentage = flt(self.process_loss_qty * 100 / self.fg_completed_qty) @@ -1573,16 +1576,36 @@ class StockEntry(StockController): if self.purpose not in ("Manufacture", "Repack"): return - self.process_loss_qty = 0.0 - if not self.process_loss_percentage: + precision = self.precision("process_loss_qty") + if self.work_order: + data = frappe.get_all( + "Work Order Operation", + filters={"parent": self.work_order}, + fields=["max(process_loss_qty) as process_loss_qty"], + ) + + if data and data[0].process_loss_qty is not None: + process_loss_qty = data[0].process_loss_qty + if flt(self.process_loss_qty, precision) != flt(process_loss_qty, precision): + self.process_loss_qty = flt(process_loss_qty, precision) + + frappe.msgprint( + _("The Process Loss Qty has reset as per job cards Process Loss Qty"), alert=True + ) + + if not self.process_loss_percentage and not self.process_loss_qty: self.process_loss_percentage = frappe.get_cached_value( "BOM", self.bom_no, "process_loss_percentage" ) - if self.process_loss_percentage: + if self.process_loss_percentage and not self.process_loss_qty: self.process_loss_qty = flt( (flt(self.fg_completed_qty) * flt(self.process_loss_percentage)) / 100 ) + else: + self.process_loss_percentage = flt( + (flt(self.process_loss_qty) / flt(self.fg_completed_qty)) * 100 + ) def set_work_order_details(self): if not getattr(self, "pro_doc", None): From 7af03800c9709b328f9b649e50edfea50edc6a6d Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sat, 10 Jun 2023 13:27:38 +0530 Subject: [PATCH 16/24] fix: test case (cherry picked from commit 0382eecff4a8005e6d013a8daf3fee1ffdeaf408) --- .../doctype/job_card/test_job_card.py | 114 ++++++++++++++++++ .../doctype/routing/test_routing.py | 1 + .../doctype/work_order/test_work_order.py | 2 +- .../stock/doctype/stock_entry/stock_entry.py | 6 +- 4 files changed, 120 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index a7f06486abc..e7fbcda7ab0 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -5,6 +5,7 @@ from typing import Literal import frappe +from frappe.test_runner import make_test_records from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import random_string from frappe.utils.data import add_to_date, now, today @@ -469,6 +470,119 @@ class TestJobCard(FrappeTestCase): self.assertEqual(ste.from_bom, 1.0) self.assertEqual(ste.bom_no, work_order.bom_no) + def test_job_card_proccess_qty_and_completed_qty(self): + from erpnext.manufacturing.doctype.routing.test_routing import ( + create_routing, + setup_bom, + setup_operations, + ) + from erpnext.manufacturing.doctype.work_order.work_order import ( + make_stock_entry as make_stock_entry_for_wo, + ) + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + operations = [ + {"operation": "Test Operation A1", "workstation": "Test Workstation A", "time_in_mins": 30}, + {"operation": "Test Operation B1", "workstation": "Test Workstation A", "time_in_mins": 20}, + ] + + make_test_records("UOM") + + warehouse = create_warehouse("Test Warehouse 123 for Job Card") + + setup_operations(operations) + + item_code = "Test Job Card Process Qty Item" + for item in [item_code, item_code + "RM 1", item_code + "RM 2"]: + if not frappe.db.exists("Item", item): + make_item( + item, + { + "item_name": item, + "stock_uom": "Nos", + "is_stock_item": 1, + }, + ) + + routing_doc = create_routing(routing_name="Testing Route", operations=operations) + bom_doc = setup_bom( + item_code=item_code, + routing=routing_doc.name, + raw_materials=[item_code + "RM 1", item_code + "RM 2"], + source_warehouse=warehouse, + ) + + for row in bom_doc.items: + make_stock_entry( + item_code=row.item_code, + target=row.source_warehouse, + qty=10, + basic_rate=100, + ) + + wo_doc = make_wo_order_test_record( + production_item=item_code, + bom_no=bom_doc.name, + skip_transfer=1, + wip_warehouse=warehouse, + source_warehouse=warehouse, + ) + + for row in routing_doc.operations: + self.assertEqual(row.sequence_id, row.idx) + + first_job_card = frappe.get_all( + "Job Card", + filters={"work_order": wo_doc.name, "sequence_id": 1}, + fields=["name"], + order_by="sequence_id", + limit=1, + )[0].name + + jc = frappe.get_doc("Job Card", first_job_card) + jc.time_logs[0].completed_qty = 8 + jc.save() + jc.submit() + + self.assertEqual(jc.process_loss_qty, 2) + self.assertEqual(jc.for_quantity, 10) + + second_job_card = frappe.get_all( + "Job Card", + filters={"work_order": wo_doc.name, "sequence_id": 2}, + fields=["name"], + order_by="sequence_id", + limit=1, + )[0].name + + jc2 = frappe.get_doc("Job Card", second_job_card) + jc2.time_logs[0].completed_qty = 10 + + self.assertRaises(frappe.ValidationError, jc2.save) + + jc2.load_from_db() + jc2.time_logs[0].completed_qty = 8 + jc2.save() + jc2.submit() + + self.assertEqual(jc2.for_quantity, 10) + self.assertEqual(jc2.process_loss_qty, 2) + + s = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 10)) + s.submit() + + self.assertEqual(s.process_loss_qty, 2) + + wo_doc.reload() + for row in wo_doc.operations: + self.assertEqual(row.completed_qty, 8) + self.assertEqual(row.process_loss_qty, 2) + + self.assertEqual(wo_doc.produced_qty, 8) + self.assertEqual(wo_doc.process_loss_qty, 2) + self.assertEqual(wo_doc.status, "Completed") + def create_bom_with_multiple_operations(): "Create a BOM with multiple operations and Material Transfer against Job Card" diff --git a/erpnext/manufacturing/doctype/routing/test_routing.py b/erpnext/manufacturing/doctype/routing/test_routing.py index 48f1851cb10..a37ff28031b 100644 --- a/erpnext/manufacturing/doctype/routing/test_routing.py +++ b/erpnext/manufacturing/doctype/routing/test_routing.py @@ -141,6 +141,7 @@ def setup_bom(**args): routing=args.routing, with_operations=1, currency=args.currency, + source_warehouse=args.source_warehouse, ) else: bom_doc = frappe.get_doc("BOM", name) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index bb53c8c225c..f1ac5d7b308 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -891,7 +891,7 @@ class TestWorkOrder(FrappeTestCase): self.assertEqual(se.process_loss_qty, 1) wo.load_from_db() - self.assertEqual(wo.status, "In Process") + self.assertEqual(wo.status, "Completed") @timeout(seconds=60) def test_job_card_scrap_item(self): diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index e74bc9e2f2b..3f5457c633e 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -594,7 +594,9 @@ class StockEntry(StockController): for d in prod_order.get("operations"): total_completed_qty = flt(self.fg_completed_qty) + flt(prod_order.produced_qty) - completed_qty = d.completed_qty + (allowance_percentage / 100 * d.completed_qty) + completed_qty = ( + d.completed_qty + d.process_loss_qty + (allowance_percentage / 100 * d.completed_qty) + ) if total_completed_qty > flt(completed_qty): job_card = frappe.db.get_value("Job Card", {"operation_id": d.name}, "name") if not job_card: @@ -1602,7 +1604,7 @@ class StockEntry(StockController): self.process_loss_qty = flt( (flt(self.fg_completed_qty) * flt(self.process_loss_percentage)) / 100 ) - else: + elif self.process_loss_qty and not self.process_loss_percentage: self.process_loss_percentage = flt( (flt(self.process_loss_qty) / flt(self.fg_completed_qty)) * 100 ) From cf14858909209fbe640803ed535004dd6138e160 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 9 Jun 2023 11:54:45 +0530 Subject: [PATCH 17/24] fix: allow user to set rounding loss allowance for accounts balance (cherry picked from commit 96a0132501ef2c5055b310c500cd9959edfcbfa8) --- .../exchange_rate_revaluation.js | 18 +++++++++- .../exchange_rate_revaluation.json | 10 +++++- .../exchange_rate_revaluation.py | 35 ++++++++++++++++--- 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js index f72ecc9e501..733a7616b21 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js @@ -35,6 +35,21 @@ frappe.ui.form.on('Exchange Rate Revaluation', { } }, + validate_rounding_loss: function(frm) { + allowance = frm.doc.rounding_loss_allowance; + if (!(allowance > 0 && allowance < 1)) { + frappe.throw(__("Rounding Loss Allowance should be between 0 and 1")); + } + }, + + rounding_loss_allowance: function(frm) { + frm.events.validate_rounding_loss(frm); + }, + + validate: function(frm) { + frm.events.validate_rounding_loss(frm); + }, + get_entries: function(frm, account) { frappe.call({ method: "get_accounts_data", @@ -126,7 +141,8 @@ var get_account_details = function(frm, cdt, cdn) { company: frm.doc.company, posting_date: frm.doc.posting_date, party_type: row.party_type, - party: row.party + party: row.party, + rounding_loss_allowance: frm.doc.rounding_loss_allowance }, callback: function(r){ $.extend(row, r.message); diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json index 0d198ca1201..2310d1272cd 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json @@ -8,6 +8,7 @@ "engine": "InnoDB", "field_order": [ "posting_date", + "rounding_loss_allowance", "column_break_2", "company", "section_break_4", @@ -96,11 +97,18 @@ { "fieldname": "column_break_10", "fieldtype": "Column Break" + }, + { + "default": "0.05", + "description": "Only values between 0 and 1 are allowed. \nEx: If allowance is set at 0.07, accounts that have balance of 0.07 in either of the currencies will be considered as zero balance account", + "fieldname": "rounding_loss_allowance", + "fieldtype": "Float", + "label": "Rounding Loss Allowance" } ], "is_submittable": 1, "links": [], - "modified": "2022-12-29 19:38:24.416529", + "modified": "2023-06-12 21:02:09.818208", "modified_by": "Administrator", "module": "Accounts", "name": "Exchange Rate Revaluation", diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index b528ee58e23..043fbdd5d6c 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -18,8 +18,13 @@ from erpnext.setup.utils import get_exchange_rate class ExchangeRateRevaluation(Document): def validate(self): + self.validate_rounding_loss_allowance() self.set_total_gain_loss() + def validate_rounding_loss_allowance(self): + if not (self.rounding_loss_allowance > 0 and self.rounding_loss_allowance < 1): + frappe.throw(_("Rounding Loss Allowance should be between 0 and 1")) + def set_total_gain_loss(self): total_gain_loss = 0 @@ -92,7 +97,12 @@ class ExchangeRateRevaluation(Document): def get_accounts_data(self): self.validate_mandatory() account_details = self.get_account_balance_from_gle( - company=self.company, posting_date=self.posting_date, account=None, party_type=None, party=None + company=self.company, + posting_date=self.posting_date, + account=None, + party_type=None, + party=None, + rounding_loss_allowance=self.rounding_loss_allowance, ) accounts_with_new_balance = self.calculate_new_account_balance( self.company, self.posting_date, account_details @@ -104,7 +114,9 @@ class ExchangeRateRevaluation(Document): return accounts_with_new_balance @staticmethod - def get_account_balance_from_gle(company, posting_date, account, party_type, party): + def get_account_balance_from_gle( + company, posting_date, account, party_type, party, rounding_loss_allowance + ): account_details = [] if company and posting_date: @@ -172,10 +184,18 @@ class ExchangeRateRevaluation(Document): ) # round off balance based on currency precision + # and consider debit-credit difference allowance currency_precision = get_currency_precision() + rounding_loss_allowance = rounding_loss_allowance or 0.05 for acc in account_details: acc.balance_in_account_currency = flt(acc.balance_in_account_currency, currency_precision) + if abs(acc.balance_in_account_currency) <= rounding_loss_allowance: + acc.balance_in_account_currency = 0 + acc.balance = flt(acc.balance, currency_precision) + if abs(acc.balance) <= rounding_loss_allowance: + acc.balance = 0 + acc.zero_balance = ( True if (acc.balance == 0 or acc.balance_in_account_currency == 0) else False ) @@ -531,7 +551,9 @@ def calculate_exchange_rate_using_last_gle(company, account, party_type, party): @frappe.whitelist() -def get_account_details(company, posting_date, account, party_type=None, party=None): +def get_account_details( + company, posting_date, account, party_type=None, party=None, rounding_loss_allowance=None +): if not (company and posting_date): frappe.throw(_("Company and Posting Date is mandatory")) @@ -549,7 +571,12 @@ def get_account_details(company, posting_date, account, party_type=None, party=N "account_currency": account_currency, } account_balance = ExchangeRateRevaluation.get_account_balance_from_gle( - company=company, posting_date=posting_date, account=account, party_type=party_type, party=party + company=company, + posting_date=posting_date, + account=account, + party_type=party_type, + party=party, + rounding_loss_allowance=rounding_loss_allowance, ) if account_balance and ( From 25b3c7736b3f99a0193cfe58bc0238a4508223a9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 13 Jun 2023 09:32:24 +0530 Subject: [PATCH 18/24] fix: attribute error on payment reconciliation tool (cherry picked from commit bada5796fac7eb5e47a7640ee75709b356dd65c7) --- .../payment_reconciliation/payment_reconciliation.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 081fe70354d..2c8faecf4b1 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -211,7 +211,13 @@ class PaymentReconciliation(Document): accounting_dimensions=self.accounting_dimension_filter_conditions, ) - cr_dr_notes = [x.voucher_no for x in self.return_invoices] + cr_dr_notes = ( + [x.voucher_no for x in self.return_invoices] + if self.party_type in ["Customer", "Supplier"] + else [] + ) + # Filter out cr/dr notes from outstanding invoices list + # Happens when non-standalone cr/dr notes are linked with another invoice through journal entry non_reconciled_invoices = [x for x in non_reconciled_invoices if x.voucher_no not in cr_dr_notes] if self.invoice_limit: From 2060a003c87264a0601bf575898d0d36e881a29d Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 13 Jun 2023 11:22:59 +0530 Subject: [PATCH 19/24] fix: conflicts --- .../manufacturing/doctype/job_card/job_card.json | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 3cd1f54b69e..dc0213a7bcd 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -436,23 +436,12 @@ "fieldname": "expected_end_date", "fieldtype": "Datetime", "label": "Expected End Date" -<<<<<<< HEAD -======= - }, - { - "fieldname": "serial_and_batch_bundle", - "fieldtype": "Link", - "label": "Serial and Batch Bundle", - "no_copy": 1, - "options": "Serial and Batch Bundle", - "print_hide": 1 }, { "fieldname": "process_loss_qty", "fieldtype": "Float", "label": "Process Loss Qty", "read_only": 1 ->>>>>>> e9a6191af9 (fix: added process loss in job card) } ], "is_submittable": 1, @@ -515,4 +504,4 @@ "states": [], "title_field": "operation", "track_changes": 1 -} \ No newline at end of file +} From dffa682b8066ae85081946a30c4841fd550584cb Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 13 Jun 2023 19:22:25 +0530 Subject: [PATCH 20/24] Stock aging report fix when called in dashboard chart (backport #35671) (#35675) fix: get_range_age conditions fixed (#35671) see https://github.com/frappe/erpnext/issues/35669 (cherry picked from commit 9f669d4c2f18613b0a8e7b58074ce0aa3438037c) Co-authored-by: Hossein Yousefian <86075967+ihosseinu@users.noreply.github.com> --- erpnext/stock/report/stock_ageing/stock_ageing.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index d3f1f31af48..d0929a082ce 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -96,14 +96,14 @@ def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: D range1 = range2 = range3 = above_range3 = 0.0 for item in fifo_queue: - age = date_diff(to_date, item[1]) + age = flt(date_diff(to_date, item[1])) qty = flt(item[0]) if not item_dict["has_serial_no"] else 1.0 - if age <= filters.range1: + if age <= flt(filters.range1): range1 = flt(range1 + qty, precision) - elif age <= filters.range2: + elif age <= flt(filters.range2): range2 = flt(range2 + qty, precision) - elif age <= filters.range3: + elif age <= flt(filters.range3): range3 = flt(range3 + qty, precision) else: above_range3 = flt(above_range3 + qty, precision) From 4add1b4374273b90cbccdf9e3096564321360651 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 13 Jun 2023 19:47:20 +0530 Subject: [PATCH 21/24] fix(accounts): validate payment entry references with latest data. (#31166) fix(accounts): validate payment entry references with latest data. (#31166) * test: payment entry over allocation. * fix: validate allocated_amount against latest outstanding amount. * fix: payment entry get outstanding documents for advance payments * fix: only fetch latest outstanding_amount. * fix: throw if reference is allocated * test: throw error if a reference has been partially allocated after inital creation. * chore: test name * fix: remove unused part of test * chore: linter * chore: more user friendly error messages * fix: only validate outstanding amount if partly paid and don't filter by cost center * chore: minor refactor for doc.cost_center Co-authored-by: Deepesh Garg --------- Co-authored-by: Anand Baburajan Co-authored-by: Deepesh Garg (cherry picked from commit 20de27d480b6c55fce0335cac15866d83b44acc1) Co-authored-by: Devin Slauenwhite --- .../doctype/payment_entry/payment_entry.py | 68 +++++++++++++++---- .../payment_entry/test_payment_entry.py | 24 +++++++ 2 files changed, 80 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 49b75dc0570..118d5636769 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -148,19 +148,57 @@ class PaymentEntry(AccountsController): ) def validate_allocated_amount(self): - for d in self.get("references"): + if self.payment_type == "Internal Transfer": + return + + latest_references = get_outstanding_reference_documents( + { + "posting_date": self.posting_date, + "company": self.company, + "party_type": self.party_type, + "payment_type": self.payment_type, + "party": self.party, + "party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to, + } + ) + + # Group latest_references by (voucher_type, voucher_no) + latest_lookup = {} + for d in latest_references: + d = frappe._dict(d) + latest_lookup.update({(d.voucher_type, d.voucher_no): d}) + + for d in self.get("references").copy(): + latest = latest_lookup.get((d.reference_doctype, d.reference_name)) + + # The reference has already been fully paid + if not latest: + frappe.throw( + _("{0} {1} has already been fully paid.").format(d.reference_doctype, d.reference_name) + ) + # The reference has already been partly paid + elif ( + latest.outstanding_amount < latest.invoice_amount + and d.outstanding_amount != latest.outstanding_amount + ): + frappe.throw( + _( + "{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' button to get the latest outstanding amount." + ).format(d.reference_doctype, d.reference_name) + ) + + d.outstanding_amount = latest.outstanding_amount + + fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.") + if (flt(d.allocated_amount)) > 0: if flt(d.allocated_amount) > flt(d.outstanding_amount): - frappe.throw( - _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx) - ) + frappe.throw(fail_message.format(d.idx)) # Check for negative outstanding invoices as well if flt(d.allocated_amount) < 0: if flt(d.allocated_amount) < flt(d.outstanding_amount): - frappe.throw( - _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx) - ) + frappe.throw(fail_message.format(d.idx)) def delink_advance_entry_references(self): for reference in self.references: @@ -373,7 +411,7 @@ class PaymentEntry(AccountsController): for k, v in no_oustanding_refs.items(): frappe.msgprint( _( - "{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry." + "{} - {} now has {} as it had no outstanding amount left before submitting the Payment Entry." ).format( _(k), frappe.bold(", ".join(d.reference_name for d in v)), @@ -1449,7 +1487,7 @@ def get_orders_to_be_billed( if voucher_type: doc = frappe.get_doc({"doctype": voucher_type}) condition = "" - if doc and hasattr(doc, "cost_center"): + if doc and hasattr(doc, "cost_center") and doc.cost_center: condition = " and cost_center='%s'" % cost_center orders = [] @@ -1495,9 +1533,15 @@ def get_orders_to_be_billed( order_list = [] for d in orders: - if not ( - flt(d.outstanding_amount) >= flt(filters.get("outstanding_amt_greater_than")) - and flt(d.outstanding_amount) <= flt(filters.get("outstanding_amt_less_than")) + if ( + filters + and filters.get("outstanding_amt_greater_than") + and filters.get("outstanding_amt_less_than") + and not ( + flt(filters.get("outstanding_amt_greater_than")) + <= flt(d.outstanding_amount) + <= flt(filters.get("outstanding_amt_less_than")) + ) ): continue diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 68f333dc29f..278b12f6595 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -1013,6 +1013,30 @@ class TestPaymentEntry(FrappeTestCase): employee = make_employee("test_payment_entry@salary.com", company="_Test Company") create_payment_entry(party_type="Employee", party=employee, save=True) + def test_duplicate_payment_entry_allocate_amount(self): + si = create_sales_invoice() + + pe_draft = get_payment_entry("Sales Invoice", si.name) + pe_draft.insert() + + pe = get_payment_entry("Sales Invoice", si.name) + pe.submit() + + self.assertRaises(frappe.ValidationError, pe_draft.submit) + + def test_duplicate_payment_entry_partial_allocate_amount(self): + si = create_sales_invoice() + + pe_draft = get_payment_entry("Sales Invoice", si.name) + pe_draft.insert() + + pe = get_payment_entry("Sales Invoice", si.name) + pe.received_amount = si.total / 2 + pe.references[0].allocated_amount = si.total / 2 + pe.submit() + + self.assertRaises(frappe.ValidationError, pe_draft.submit) + def create_payment_entry(**args): payment_entry = frappe.new_doc("Payment Entry") From f39ae9dbb1e9dc55927d104d91e982578a35623e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 13 Jun 2023 20:44:05 +0530 Subject: [PATCH 22/24] fix: make showing taxes as table in print configurable (backport #35672) (#35678) * fix: make showing taxes as table in print configurable (#35672) (cherry picked from commit 491a50a02766d833eec0b2cad8650ef495206a8e) # Conflicts: # erpnext/accounts/doctype/accounts_settings/accounts_settings.json * chore: fix conflict --------- Co-authored-by: Anand Baburajan --- .../doctype/accounts_settings/accounts_settings.json | 11 +++++++++-- erpnext/controllers/accounts_controller.py | 3 +++ erpnext/controllers/print_settings.py | 8 +++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index d5ed0676fd1..6c99d29dbfc 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -36,6 +36,7 @@ "book_tax_discount_loss", "print_settings", "show_inclusive_tax_in_print", + "show_taxes_as_table_in_print", "column_break_12", "show_payment_schedule_in_print", "currency_exchange_section", @@ -378,6 +379,12 @@ "fieldname": "auto_reconcile_payments", "fieldtype": "Check", "label": "Auto Reconcile Payments" + }, + { + "default": "0", + "fieldname": "show_taxes_as_table_in_print", + "fieldtype": "Check", + "label": "Show Taxes as Table in Print" } ], "icon": "icon-cog", @@ -385,7 +392,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-04-21 13:11:37.130743", + "modified": "2023-06-13 18:47:46.430291", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", @@ -414,4 +421,4 @@ "sort_order": "ASC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 10788c36eec..a1eba4ae0c3 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -917,6 +917,9 @@ class AccountsController(TransactionBase): return is_inclusive + def should_show_taxes_as_table_in_print(self): + return cint(frappe.db.get_single_value("Accounts Settings", "show_taxes_as_table_in_print")) + def validate_advance_entries(self): order_field = "sales_order" if self.doctype == "Sales Invoice" else "purchase_order" order_list = list(set(d.get(order_field) for d in self.get("items") if d.get(order_field))) diff --git a/erpnext/controllers/print_settings.py b/erpnext/controllers/print_settings.py index d2c80961a36..c951154a9e0 100644 --- a/erpnext/controllers/print_settings.py +++ b/erpnext/controllers/print_settings.py @@ -30,10 +30,16 @@ def set_print_templates_for_taxes(doc, settings): doc.print_templates.update( { "total": "templates/print_formats/includes/total.html", - "taxes": "templates/print_formats/includes/taxes.html", } ) + if not doc.should_show_taxes_as_table_in_print(): + doc.print_templates.update( + { + "taxes": "templates/print_formats/includes/taxes.html", + } + ) + def format_columns(display_columns, compact_fields): compact_fields = compact_fields + ["image", "item_code", "item_name"] From 6f59fa9e5bc4246cd4f1039f82da5577a26d4cfa Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 14 Jun 2023 09:01:04 +0530 Subject: [PATCH 23/24] fix: Lower deduction certificate not getting applied (#35667) * fix: Lower deduction certificate not getting applied (#35667) (cherry picked from commit 937c0feefe302d0a63fca2bee84da1580f7b6e26) # Conflicts: # erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py * chore: resolve conflicts --------- Co-authored-by: Deepesh Garg --- .../tax_withholding_category.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 1f2d9803739..d8827e09662 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -5,7 +5,7 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cint, getdate +from frappe.utils import cint, flt, getdate class TaxWithholdingCategory(Document): @@ -569,7 +569,12 @@ def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total): tds_amount = 0 limit_consumed = frappe.db.get_value( "Purchase Invoice", - {"supplier": ("in", parties), "apply_tds": 1, "docstatus": 1}, + { + "supplier": ("in", parties), + "apply_tds": 1, + "docstatus": 1, + "posting_date": ("between", (ldc.valid_from, ldc.valid_upto)), + }, "sum(tax_withholding_net_total)", ) @@ -584,10 +589,10 @@ def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total): def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details): - if current_amount < (certificate_limit - deducted_amount): + if certificate_limit - flt(deducted_amount) - flt(current_amount) >= 0: return current_amount * rate / 100 else: - ltds_amount = certificate_limit - deducted_amount + ltds_amount = certificate_limit - flt(deducted_amount) tds_amount = current_amount - ltds_amount return ltds_amount * rate / 100 + tds_amount * tax_details.rate / 100 @@ -598,9 +603,9 @@ def is_valid_certificate( ): valid = False - if ( - getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto) - ) and certificate_limit > deducted_amount: + available_amount = flt(certificate_limit) - flt(deducted_amount) - flt(current_amount) + + if (getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)) and available_amount > 0: valid = True return valid From 4a8ce226f6204bf1ed484ffb2c3689ddccc19468 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 14 Jun 2023 09:01:38 +0530 Subject: [PATCH 24/24] fix: Validation for delivery date in Sales Order (#35597) fix: Validation for delivery date in Sales Order (#35597) * fix: Validation for delivery date in Sales Order * chore: update utils * chore: revert * chore: Add default delivery date (cherry picked from commit 984f89d274289535e05c53ae8dc47ef4454a65e3) Co-authored-by: Deepesh Garg --- erpnext/selling/doctype/quotation/quotation.py | 2 ++ erpnext/selling/doctype/quotation/test_quotation.py | 4 +--- erpnext/selling/doctype/sales_order/sales_order.py | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 61969fe8a91..8ff681b0481 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -299,6 +299,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): ) target.flags.ignore_permissions = ignore_permissions + target.delivery_date = nowdate() target.run_method("set_missing_values") target.run_method("calculate_taxes_and_totals") @@ -306,6 +307,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): balance_qty = obj.qty - ordered_items.get(obj.item_code, 0.0) target.qty = balance_qty if balance_qty > 0 else 0 target.stock_qty = flt(target.qty) * flt(obj.conversion_factor) + target.delivery_date = nowdate() if obj.against_blanket_order: target.against_blanket_order = obj.against_blanket_order diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 67f6518657e..5623a12cdda 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -60,9 +60,9 @@ class TestQuotation(FrappeTestCase): sales_order = make_sales_order(quotation.name) sales_order.currency = "USD" sales_order.conversion_rate = 20.0 - sales_order.delivery_date = "2019-01-01" sales_order.naming_series = "_T-Quotation-" sales_order.transaction_date = nowdate() + sales_order.delivery_date = nowdate() sales_order.insert() self.assertEqual(sales_order.currency, "USD") @@ -644,8 +644,6 @@ def make_quotation(**args): }, ) - qo.delivery_date = add_days(qo.transaction_date, 10) - if not args.do_not_save: qo.insert() if not args.do_not_submit: diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 92b469c6229..97cccb13620 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -158,7 +158,8 @@ class SalesOrder(SellingController): frappe.msgprint( _("Expected Delivery Date should be after Sales Order Date"), indicator="orange", - title=_("Warning"), + title=_("Invalid Delivery Date"), + raise_exception=True, ) else: frappe.throw(_("Please enter Delivery Date"))