From 7e344685043bfba06ed4c74f7c3b006d87ffaa9c Mon Sep 17 00:00:00 2001 From: "Nihantra C. Patel" <141945075+Nihantra-Patel@users.noreply.github.com> Date: Fri, 11 Aug 2023 12:27:18 +0530 Subject: [PATCH 01/60] fix: POS Invoice Email Receipt Mail --- erpnext/selling/page/point_of_sale/pos_past_order_summary.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js index be75bd64cf..cfad587098 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js @@ -49,7 +49,7 @@ erpnext.PointOfSale.PastOrderSummary = class { title: 'Email Receipt', fields: [ {fieldname: 'email_id', fieldtype: 'Data', options: 'Email', label: 'Email ID'}, - // {fieldname:'remarks', fieldtype:'Text', label:'Remarks (if any)'} + {fieldname:'content', fieldtype:'Small Text', label:'Message (if any)'} ], primary_action: () => { this.send_email(); @@ -243,6 +243,7 @@ erpnext.PointOfSale.PastOrderSummary = class { send_email() { const frm = this.events.get_frm(); const recipients = this.email_dialog.get_values().email_id; + const content = this.email_dialog.get_values().content; const doc = this.doc || frm.doc; const print_format = frm.pos_print_format; @@ -251,6 +252,7 @@ erpnext.PointOfSale.PastOrderSummary = class { args: { recipients: recipients, subject: __(frm.meta.name) + ': ' + doc.name, + content: content ? content : __(frm.meta.name) + ': ' + doc.name, doctype: doc.doctype, name: doc.name, send_email: 1, From dd91a77fdd3ea6ed88d9afd02285bfe38e90a717 Mon Sep 17 00:00:00 2001 From: "Nihantra C. Patel" <141945075+Nihantra-Patel@users.noreply.github.com> Date: Fri, 11 Aug 2023 13:19:09 +0530 Subject: [PATCH 02/60] fix: POS Invoice Email Receipt Mail --- erpnext/selling/page/point_of_sale/pos_past_order_summary.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js index cfad587098..d341d23bd3 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js @@ -48,7 +48,7 @@ erpnext.PointOfSale.PastOrderSummary = class { const email_dialog = new frappe.ui.Dialog({ title: 'Email Receipt', fields: [ - {fieldname: 'email_id', fieldtype: 'Data', options: 'Email', label: 'Email ID'}, + {fieldname: 'email_id', fieldtype: 'Data', options: 'Email', label: 'Email ID', reqd: 1}, {fieldname:'content', fieldtype:'Small Text', label:'Message (if any)'} ], primary_action: () => { From db76e8a277f4f62dde079b2429f02689ea3fc25b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 4 May 2023 16:27:43 +0530 Subject: [PATCH 03/60] refactor: cr notes will post for itself --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 4 +--- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index f33439989a..37d1ec6267 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -628,9 +628,7 @@ class PurchaseInvoice(BuyingController): "credit_in_account_currency": base_grand_total if self.party_account_currency == self.company_currency else grand_total, - "against_voucher": self.return_against - if cint(self.is_return) and self.return_against - else self.name, + "against_voucher": self.name, "against_voucher_type": self.doctype, "project": self.project, "cost_center": self.cost_center, diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 0bc5aa2ed2..d03afe36b1 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1104,9 +1104,7 @@ class SalesInvoice(SellingController): "debit_in_account_currency": base_grand_total if self.party_account_currency == self.company_currency else grand_total, - "against_voucher": self.return_against - if cint(self.is_return) and self.return_against - else self.name, + "against_voucher": self.name, "against_voucher_type": self.doctype, "cost_center": self.cost_center, "project": self.project, From 00878707aecb9ee7efae49d1dea81150b9adbb1a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 4 Jul 2023 15:59:57 +0530 Subject: [PATCH 04/60] refactor: remove return_against for cr/dr note filter --- .../payment_reconciliation/payment_reconciliation.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index ea06e0ec9a..8c84f836a5 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -174,15 +174,12 @@ class PaymentReconciliation(Document): self.common_filter_conditions.append(ple.account == self.receivable_payable_account) 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: + if self.return_invoices: ple_query = QueryPaymentLedger() return_outstanding = ple_query.get_voucher_outstandings( - vouchers=return_invoices, + vouchers=self.return_invoices, common_filter=self.common_filter_conditions, posting_date=self.ple_posting_date_filter, min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None, From b30c1e1abfcb45950709e7b12481d9e187b845f4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 13 Aug 2023 15:48:25 +0530 Subject: [PATCH 05/60] refactor(test): return invoice will have -ve outstanding --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 63c0c45304..b310f3b788 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1500,8 +1500,8 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(party_credited, 1000) # Check outstanding amount - self.assertFalse(si1.outstanding_amount) - self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 1500) + self.assertEqual(frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount"), -1000) + self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 2500) def test_gle_made_when_asset_is_returned(self): create_asset_data() From 0e2fb1188a66d6f1e6fe89564106c024e940107f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 15 Aug 2023 09:02:41 +0530 Subject: [PATCH 06/60] refactor(test): ledger entries will be against itself --- .../test_payment_ledger_entry.py | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py b/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py index fc6dbba7e7..ce9579ed61 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py +++ b/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py @@ -294,7 +294,7 @@ class TestPaymentLedgerEntry(FrappeTestCase): cr_note1.return_against = si3.name cr_note1 = cr_note1.save().submit() - pl_entries = ( + pl_entries_si3 = ( qb.from_(ple) .select( ple.voucher_type, @@ -309,7 +309,24 @@ class TestPaymentLedgerEntry(FrappeTestCase): .run(as_dict=True) ) - expected_values = [ + pl_entries_cr_note1 = ( + qb.from_(ple) + .select( + ple.voucher_type, + ple.voucher_no, + ple.against_voucher_type, + ple.against_voucher_no, + ple.amount, + ple.delinked, + ) + .where( + (ple.against_voucher_type == cr_note1.doctype) & (ple.against_voucher_no == cr_note1.name) + ) + .orderby(ple.creation) + .run(as_dict=True) + ) + + expected_values_for_si3 = [ { "voucher_type": si3.doctype, "voucher_no": si3.name, @@ -317,18 +334,21 @@ class TestPaymentLedgerEntry(FrappeTestCase): "against_voucher_no": si3.name, "amount": amount, "delinked": 0, - }, + } + ] + # credit/debit notes post ledger entries against itself + expected_values_for_cr_note1 = [ { "voucher_type": cr_note1.doctype, "voucher_no": cr_note1.name, - "against_voucher_type": si3.doctype, - "against_voucher_no": si3.name, + "against_voucher_type": cr_note1.doctype, + "against_voucher_no": cr_note1.name, "amount": -amount, "delinked": 0, }, ] - self.assertEqual(pl_entries[0], expected_values[0]) - self.assertEqual(pl_entries[1], expected_values[1]) + self.assertEqual(pl_entries_si3, expected_values_for_si3) + self.assertEqual(pl_entries_cr_note1, expected_values_for_cr_note1) def test_je_against_inv_and_note(self): ple = self.ple From 43f530b077be3fc5b6f7a1cd70d41b0b74aa8472 Mon Sep 17 00:00:00 2001 From: Kevin Shenk Date: Tue, 15 Aug 2023 13:34:59 -0400 Subject: [PATCH 07/60] fix: Plaid Integration status and categories "pending" is a boolean not a string, and "category" doesn't exist in some edge cases --- .../doctype/plaid_settings/plaid_settings.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index 61d2acefae..11d5f6a9c4 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -237,14 +237,15 @@ def new_bank_transaction(transaction): deposit = abs(amount) withdrawal = 0.0 - status = "Pending" if transaction["pending"] == "True" else "Settled" + status = "Pending" if transaction["pending"] == True else "Settled" tags = [] - try: - tags += transaction["category"] - tags += [f'Plaid Cat. {transaction["category_id"]}'] - except KeyError: - pass + if transaction["category"]: + try: + tags += transaction["category"] + tags += [f'Plaid Cat. {transaction["category_id"]}'] + except KeyError: + pass if not frappe.db.exists("Bank Transaction", dict(transaction_id=transaction["transaction_id"])): try: From f6e4ac2b6267ac0b88f399ada1482fc1366a126a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 4 Jul 2023 16:31:10 +0530 Subject: [PATCH 08/60] refactor(test): payments to invoice with -ve outstanding --- .../payment_entry/test_payment_entry.py | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 8f9f7ce3be..c8bf6644a5 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -702,7 +702,50 @@ class TestPaymentEntry(FrappeTestCase): pe2.submit() # create return entry against si1 - create_sales_invoice(is_return=1, return_against=si1.name, qty=-1) + cr_note = create_sales_invoice(is_return=1, return_against=si1.name, qty=-1) + si1_outstanding = frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount") + + # create JE(credit note) manually against si1 and cr_note + je = frappe.get_doc( + { + "doctype": "Journal Entry", + "company": si1.company, + "voucher_type": "Credit Note", + "posting_date": nowdate(), + } + ) + je.append( + "accounts", + { + "account": si1.debit_to, + "party_type": "Customer", + "party": si1.customer, + "debit": 0, + "credit": 100, + "debit_in_account_currency": 0, + "credit_in_account_currency": 100, + "reference_type": si1.doctype, + "reference_name": si1.name, + "cost_center": si1.items[0].cost_center, + }, + ) + je.append( + "accounts", + { + "account": cr_note.debit_to, + "party_type": "Customer", + "party": cr_note.customer, + "debit": 100, + "credit": 0, + "debit_in_account_currency": 100, + "credit_in_account_currency": 0, + "reference_type": cr_note.doctype, + "reference_name": cr_note.name, + "cost_center": cr_note.items[0].cost_center, + }, + ) + je.save().submit() + si1_outstanding = frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount") self.assertEqual(si1_outstanding, -100) From 1bdd43d0f6aaaabdd18cc3e35869a968ecb07049 Mon Sep 17 00:00:00 2001 From: Ritvik Sardana Date: Thu, 17 Aug 2023 17:29:01 +0530 Subject: [PATCH 09/60] fix: mode of payment fetched from pos profile company in POS --- erpnext/accounts/doctype/pos_invoice/pos_invoice.js | 1 + erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js index 6f0b8019b8..ae132eb6eb 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js @@ -131,6 +131,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex args: { "pos_profile": frm.pos_profile }, callback: ({ message: profile }) => { this.update_customer_groups_settings(profile?.customer_groups); + this.frm.set_value("company", profile?.company); }, }); } diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 4b2fcec757..ecf9af1f17 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -49,6 +49,7 @@ class POSInvoice(SalesInvoice): self.validate_pos() self.validate_payment_amount() self.validate_loyalty_transaction() + self.validate_company_with_pos_company() if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code @@ -281,6 +282,14 @@ class POSInvoice(SalesInvoice): if total_amount_in_payments and total_amount_in_payments < invoice_total: frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total)) + def validate_company_with_pos_company(self): + if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"): + frappe.throw( + _("Company {} does not match with POS Profile Company {}").format( + self.company, frappe.db.get_value("POS Profile", self.pos_profile, "company") + ) + ) + def validate_loyalty_transaction(self): if self.redeem_loyalty_points and ( not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center @@ -359,6 +368,7 @@ class POSInvoice(SalesInvoice): profile = {} if self.pos_profile: profile = frappe.get_doc("POS Profile", self.pos_profile) + self.company = profile.get("company") if not self.get("payments") and not for_validate: update_multi_mode_option(self, profile) From 8f695123cd1e1e931b9ccac314687e010db1524d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 18 Aug 2023 10:10:42 +0530 Subject: [PATCH 10/60] refactor: criteria for `Credit Note Issued` and `Debit Note Issued` --- .../accounts/doctype/purchase_invoice/purchase_invoice.py | 8 ++------ erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 37d1ec6267..9f1224d65e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1642,12 +1642,8 @@ class PurchaseInvoice(BuyingController): elif outstanding_amount > 0 and getdate(self.due_date) >= getdate(): self.status = "Unpaid" # Check if outstanding amount is 0 due to debit note issued against invoice - elif ( - outstanding_amount <= 0 - and self.is_return == 0 - and frappe.db.get_value( - "Purchase Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1} - ) + elif self.is_return == 0 and frappe.db.get_value( + "Purchase Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1} ): self.status = "Debit Note Issued" elif self.is_return == 1: diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index d03afe36b1..fba2fa7552 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1730,12 +1730,8 @@ class SalesInvoice(SellingController): elif outstanding_amount > 0 and getdate(self.due_date) >= getdate(): self.status = "Unpaid" # Check if outstanding amount is 0 due to credit note issued against invoice - elif ( - outstanding_amount <= 0 - and self.is_return == 0 - and frappe.db.get_value( - "Sales Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1} - ) + elif self.is_return == 0 and frappe.db.get_value( + "Sales Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1} ): self.status = "Credit Note Issued" elif self.is_return == 1: From 268c19e745c80ac586ebf1ed477fe6459dfc39f3 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 18 Aug 2023 13:14:53 +0530 Subject: [PATCH 11/60] fix: don't throw if item does not have default BOM --- .../doctype/production_plan/production_plan.py | 2 +- erpnext/manufacturing/doctype/work_order/work_order.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 261aa76b70..654f37ef45 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -347,7 +347,7 @@ class ProductionPlan(Document): if not data.pending_qty: continue - item_details = get_item_details(data.item_code) + item_details = get_item_details(data.item_code, throw=False) if self.combine_items: if item_details.bom_no in refs: refs[item_details.bom_no]["so_details"].append( diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 7c15bf9234..5ad79f94b7 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1082,7 +1082,7 @@ def get_bom_operations(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() -def get_item_details(item, project=None, skip_bom_info=False): +def get_item_details(item, project=None, skip_bom_info=False, throw=True): res = frappe.db.sql( """ select stock_uom, description, item_name, allow_alternative_item, @@ -1118,12 +1118,15 @@ def get_item_details(item, project=None, skip_bom_info=False): if not res["bom_no"]: if project: - res = get_item_details(item) + res = get_item_details(item, throw=throw) frappe.msgprint( _("Default BOM not found for Item {0} and Project {1}").format(item, project), alert=1 ) else: - frappe.throw(_("Default BOM for {0} not found").format(item)) + msg = _("Default BOM for {0} not found").format(item) + frappe.msgprint(msg, raise_exception=throw, indicator="yellow", alert=(not throw)) + + return res bom_data = frappe.db.get_value( "BOM", From 2e22b019a0d15dacb3881fdc6a9fc5ccb476a355 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 18 Aug 2023 13:24:49 +0530 Subject: [PATCH 12/60] fix: throw if `BOM No` is not set --- .../manufacturing/doctype/production_plan/production_plan.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 654f37ef45..131f438e20 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -795,6 +795,9 @@ class ProductionPlan(Document): if not row.item_code: frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx)) + if not row.bom_no: + frappe.throw(_("Row #{0}: Please select the BOM No in Assembly Items").format(row.idx)) + bom_data = [] warehouse = row.warehouse if self.skip_available_sub_assembly_item else None From 96847db0ec78add9eaf781bef66464af6e13e07f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 18 Aug 2023 14:04:46 +0530 Subject: [PATCH 13/60] fix: broken consolidated report due to finance book filter --- .../consolidated_financial_statement.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 080e45a798..0051ba6aa8 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -744,13 +744,18 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters, d): if from_date: additional_conditions.append(gle.posting_date >= from_date) - finance_book = filters.get("finance_book") - company_fb = frappe.get_cached_value("Company", d.name, "default_finance_book") + finance_books = [] + finance_books.append("") + if filter_fb := filters.get("finance_book"): + finance_books.append(filter_fb) if filters.get("include_default_book_entries"): - additional_conditions.append((gle.finance_book.isin([finance_book, company_fb, "", None]))) + if company_fb := frappe.get_cached_value("Company", d.name, "default_finance_book"): + finance_books.append(company_fb) + + additional_conditions.append((gle.finance_book.isin(finance_books)) | gle.finance_book.isnull()) else: - additional_conditions.append((gle.finance_book.isin([finance_book, "", None]))) + additional_conditions.append((gle.finance_book.isin(finance_books)) | gle.finance_book.isnull()) return additional_conditions From f10a93f6ee2bb32d89647007eecf7b5ae74c7f61 Mon Sep 17 00:00:00 2001 From: avc <94137451+git-avc@users.noreply.github.com> Date: Fri, 18 Aug 2023 17:33:37 +0200 Subject: [PATCH 14/60] fix: lost reason opportunity dialog don't appears Fix #36719 --- erpnext/crm/doctype/opportunity/opportunity.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js index 6ef82971f5..0b485bb896 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.js +++ b/erpnext/crm/doctype/opportunity/opportunity.js @@ -1,7 +1,7 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt frappe.provide("erpnext.crm"); -erpnext.pre_sales.set_as_lost("Quotation"); +erpnext.pre_sales.set_as_lost("Opportunity"); erpnext.sales_common.setup_selling_controller(); From 87d02511a32ef4500c2d84ac3e25b5e912b14ec5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 19 Aug 2023 15:37:33 +0530 Subject: [PATCH 15/60] fix: timeout error coming during reposting (backport #36715) (#36717) fix: timeout error coming during reposting (#36715) (cherry picked from commit 620b21fec59e40178fde96085003d419804f21e0) Co-authored-by: rohitwaghchaure --- erpnext/stock/stock_ledger.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 248b7056a0..258a503dd5 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -647,7 +647,7 @@ class update_entries_after(object): def update_distinct_item_warehouses(self, dependant_sle): key = (dependant_sle.item_code, dependant_sle.warehouse) - val = frappe._dict({"sle": dependant_sle, "dependent_voucher_detail_nos": []}) + val = frappe._dict({"sle": dependant_sle}) if key not in self.distinct_item_warehouses: self.distinct_item_warehouses[key] = val @@ -661,6 +661,8 @@ class update_entries_after(object): if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date): val.sle_changed = True + dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no) + val.dependent_voucher_detail_nos = dependent_voucher_detail_nos self.distinct_item_warehouses[key] = val self.new_items_found = True elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos): From e3104f189844043c0c06f3d7f622391f83a0956f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 20 Aug 2023 20:13:31 +0530 Subject: [PATCH 16/60] fix: include gain/loss journal in AR/AP reports --- .../report/accounts_receivable/accounts_receivable.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index f78a84086a..a7b35a579a 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -1090,7 +1090,10 @@ class ReceivablePayableReport(object): .where( (je.company == self.filters.company) & (je.posting_date.lte(self.filters.report_date)) - & (je.voucher_type == "Exchange Rate Revaluation") + & ( + (je.voucher_type == "Exchange Rate Revaluation") + | (je.voucher_type == "Exchange Gain Or Loss") + ) ) .run() ) From 896b123fb116d48f3632ca9463ee9ecc4f46f945 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 20 Aug 2023 21:17:27 +0530 Subject: [PATCH 17/60] fix: broken advance field in Accounts Receivable summary rpt --- erpnext/accounts/party.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 0d67752ba7..a295f6315a 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -950,7 +950,7 @@ def get_partywise_advanced_payment_amount( if party: query = query.where(gle.party == party) - data = query.run(as_dict=True) + data = query.run() if data: return frappe._dict(data) From 0dc5e5c4303a64c52df5d9858c0de8fee576268e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 21 Aug 2023 08:46:55 +0530 Subject: [PATCH 18/60] refactor: use payment ledger to fetch advance amount --- erpnext/accounts/party.py | 29 +++++++++---------- .../accounts_receivable_summary.py | 1 - 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index a295f6315a..8bd7b5a3fe 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -14,7 +14,7 @@ from frappe.contacts.doctype.address.address import ( from frappe.contacts.doctype.contact.contact import get_contact_details from frappe.core.doctype.user_permission.user_permission import get_permitted_documents from frappe.model.utils import get_fetch_values -from frappe.query_builder.functions import Date, Sum +from frappe.query_builder.functions import Abs, Date, Sum from frappe.utils import ( add_days, add_months, @@ -922,33 +922,32 @@ def get_party_shipping_address(doctype: str, name: str) -> Optional[str]: def get_partywise_advanced_payment_amount( - party_type, posting_date=None, future_payment=0, company=None, party=None, account_type=None + party_type, posting_date=None, future_payment=0, company=None, party=None ): - gle = frappe.qb.DocType("GL Entry") + ple = frappe.qb.DocType("Payment Ledger Entry") query = ( - frappe.qb.from_(gle) - .select(gle.party) + frappe.qb.from_(ple) + .select(ple.party, Abs(Sum(ple.amount).as_("amount"))) .where( - (gle.party_type.isin(party_type)) & (gle.against_voucher.isnull()) & (gle.is_cancelled == 0) + (ple.party_type.isin(party_type)) + & (ple.amount < 0) + & (ple.against_voucher_no == ple.voucher_no) + & (ple.delinked == 0) ) - .groupby(gle.party) + .groupby(ple.party) ) - if account_type == "Receivable": - query = query.select(Sum(gle.credit).as_("amount")) - else: - query = query.select(Sum(gle.debit).as_("amount")) if posting_date: if future_payment: - query = query.where((gle.posting_date <= posting_date) | (Date(gle.creation) <= posting_date)) + query = query.where((ple.posting_date <= posting_date) | (Date(ple.creation) <= posting_date)) else: - query = query.where(gle.posting_date <= posting_date) + query = query.where(ple.posting_date <= posting_date) if company: - query = query.where(gle.company == company) + query = query.where(ple.company == company) if party: - query = query.where(gle.party == party) + query = query.where(ple.party == party) data = query.run() if data: diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index da4c9dabbf..3675e80b54 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -50,7 +50,6 @@ class AccountsReceivableSummary(ReceivablePayableReport): self.filters.show_future_payments, self.filters.company, party=party, - account_type=self.account_type, ) or {} ) From faf9f13215c834ef951fc26303ff596727cb3e67 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 21 Aug 2023 15:51:57 +0530 Subject: [PATCH 19/60] feat: allow `Update Items` for Subcontracted PO not having SCO created --- .../doctype/purchase_order/purchase_order.js | 3 +- .../doctype/purchase_order/purchase_order.py | 13 ++++++++ erpnext/public/js/utils.js | 31 +++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 7c33056a91..f6a1951439 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -185,8 +185,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e if(!in_list(["Closed", "Delivered"], doc.status)) { if(this.frm.doc.status !== 'Closed' && flt(this.frm.doc.per_received, 2) < 100 && flt(this.frm.doc.per_billed, 2) < 100) { - // Don't add Update Items button if the PO is following the new subcontracting flow. - if (!(this.frm.doc.is_subcontracted && !this.frm.doc.is_old_subcontracting_flow)) { + if (!this.frm.doc.__onload || this.frm.doc.__onload.can_update_items) { this.frm.add_custom_button(__('Update Items'), () => { erpnext.utils.update_child_items({ frm: this.frm, diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 06b9d29e69..4da1e76c2d 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -49,9 +49,22 @@ class PurchaseOrder(BuyingController): } ] + def can_update_items(self) -> bool: + result = True + + if self.is_subcontracted and not self.is_old_subcontracting_flow: + # Check - 1: NOT ALLOWED if non-cancelled Subcontracting Order exists for this Purchase Order + if frappe.db.exists( + "Subcontracting Order", {"purchase_order": self.name, "docstatus": ["!=", 2]} + ): + return False + + return result + def onload(self): supplier_tds = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category") self.set_onload("supplier_tds", supplier_tds) + self.set_onload("can_update_items", self.can_update_items()) def validate(self): super(PurchaseOrder, self).validate() diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index f456e5e500..9116fd1f03 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -678,6 +678,37 @@ erpnext.utils.update_child_items = function(opts) { }) } + if (frm.doc.doctype == 'Purchase Order' && frm.doc.is_subcontracted && !frm.doc.is_old_subcontracting_flow) { + fields.push({ + fieldtype:'Link', + fieldname:'fg_item', + options: 'Item', + reqd: 1, + in_list_view: 0, + read_only: 0, + disabled: 0, + label: __('Finished Good Item'), + get_query: () => { + return { + filters: { + 'is_stock_item': 1, + 'is_sub_contracted_item': 1, + 'default_bom': ['!=', ''] + } + } + }, + }, { + fieldtype:'Float', + fieldname:'fg_item_qty', + reqd: 1, + default: 0, + read_only: 0, + in_list_view: 0, + label: __('Finished Good Item Qty'), + precision: get_precision('fg_item_qty') + }) + } + let dialog = new frappe.ui.Dialog({ title: __("Update Items"), size: "extra-large", From 86cac1e1d2b2600c4bfb7e5535ab21847f00330e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 21 Aug 2023 15:59:52 +0530 Subject: [PATCH 20/60] fix: add missing items labels back (#36737) [skip ci] --- erpnext/accounts/doctype/sales_invoice/sales_invoice.json | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 7581366bc0..e5adeae501 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -716,6 +716,7 @@ "fieldtype": "Table", "hide_days": 1, "hide_seconds": 1, + "label": "Items", "oldfieldname": "entries", "oldfieldtype": "Table", "options": "Sales Invoice Item", From af52f21ecefa35a1ab7cdec4f7d64156933d91eb Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 21 Aug 2023 14:54:03 +0530 Subject: [PATCH 21/60] test: add test for receivable summary report --- .../test_accounts_receivable_summary.py | 203 ++++++++++++++++++ erpnext/accounts/test/accounts_mixin.py | 13 ++ 2 files changed, 216 insertions(+) create mode 100644 erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py diff --git a/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py new file mode 100644 index 0000000000..3ee35a114d --- /dev/null +++ b/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py @@ -0,0 +1,203 @@ +import unittest + +import frappe +from frappe.tests.utils import FrappeTestCase, change_settings +from frappe.utils import today + +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_summary import execute +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin + + +class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): + def setUp(self): + self.maxDiff = None + self.create_company() + self.create_customer() + self.create_item() + self.clear_old_entries() + + def tearDown(self): + frappe.db.rollback() + + def test_01_receivable_summary_output(self): + """ + Test for Invoices, Paid, Advance and Outstanding + """ + filters = { + "company": self.company, + "customer": self.customer, + "posting_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=200, + price_list_rate=200, + ) + + customer_group, customer_territory = frappe.db.get_all( + "Customer", + filters={"name": self.customer}, + fields=["customer_group", "territory"], + as_list=True, + )[0] + + report = execute(filters) + rpt_output = report[1] + expected_data = { + "party_type": "Customer", + "advance": 0, + "party": self.customer, + "invoiced": 200.0, + "paid": 0.0, + "credit_note": 0.0, + "outstanding": 200.0, + "range1": 200.0, + "range2": 0.0, + "range3": 0.0, + "range4": 0.0, + "range5": 0.0, + "total_due": 200.0, + "future_amount": 0.0, + "sales_person": [], + "currency": si.currency, + "territory": customer_territory, + "customer_group": customer_group, + } + + self.assertEqual(len(rpt_output), 1) + self.assertDictEqual(rpt_output[0], expected_data) + + # simulate advance payment + pe = get_payment_entry(si.doctype, si.name) + pe.paid_amount = 50 + pe.references[0].allocated_amount = 0 # this essitially removes the reference + pe.save().submit() + + # update expected data with advance + expected_data.update( + { + "advance": 50.0, + "outstanding": 150.0, + "range1": 150.0, + "total_due": 150.0, + } + ) + + report = execute(filters) + rpt_output = report[1] + self.assertEqual(len(rpt_output), 1) + self.assertDictEqual(rpt_output[0], expected_data) + + # make partial payment + pe = get_payment_entry(si.doctype, si.name) + pe.paid_amount = 125 + pe.references[0].allocated_amount = 125 + pe.save().submit() + + # update expected data after advance and partial payment + expected_data.update( + {"advance": 50.0, "paid": 125.0, "outstanding": 25.0, "range1": 25.0, "total_due": 25.0} + ) + + report = execute(filters) + rpt_output = report[1] + self.assertEqual(len(rpt_output), 1) + self.assertDictEqual(rpt_output[0], expected_data) + + @change_settings("Selling Settings", {"cust_master_name": "Naming Series"}) + def test_02_various_filters_and_output(self): + filters = { + "company": self.company, + "customer": self.customer, + "posting_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=200, + price_list_rate=200, + ) + # make partial payment + pe = get_payment_entry(si.doctype, si.name) + pe.paid_amount = 150 + pe.references[0].allocated_amount = 150 + pe.save().submit() + + customer_group, customer_territory = frappe.db.get_all( + "Customer", + filters={"name": self.customer}, + fields=["customer_group", "territory"], + as_list=True, + )[0] + + report = execute(filters) + rpt_output = report[1] + expected_data = { + "party_type": "Customer", + "advance": 0, + "party": self.customer, + "party_name": self.customer, + "invoiced": 200.0, + "paid": 150.0, + "credit_note": 0.0, + "outstanding": 50.0, + "range1": 50.0, + "range2": 0.0, + "range3": 0.0, + "range4": 0.0, + "range5": 0.0, + "total_due": 50.0, + "future_amount": 0.0, + "sales_person": [], + "currency": si.currency, + "territory": customer_territory, + "customer_group": customer_group, + } + + self.assertEqual(len(rpt_output), 1) + self.assertDictEqual(rpt_output[0], expected_data) + + # with gl balance filter + filters.update({"show_gl_balance": True}) + expected_data.update({"gl_balance": 50.0, "diff": 0.0}) + report = execute(filters) + rpt_output = report[1] + self.assertEqual(len(rpt_output), 1) + self.assertDictEqual(rpt_output[0], expected_data) + + # with gl balance and future payments filter + filters.update({"show_future_payments": True}) + expected_data.update({"remaining_balance": 50.0}) + report = execute(filters) + rpt_output = report[1] + self.assertEqual(len(rpt_output), 1) + self.assertDictEqual(rpt_output[0], expected_data) + + # invoice fully paid + pe = get_payment_entry(si.doctype, si.name).save().submit() + report = execute(filters) + rpt_output = report[1] + self.assertEqual(len(rpt_output), 0) diff --git a/erpnext/accounts/test/accounts_mixin.py b/erpnext/accounts/test/accounts_mixin.py index 70bbf7e694..debfffdcbb 100644 --- a/erpnext/accounts/test/accounts_mixin.py +++ b/erpnext/accounts/test/accounts_mixin.py @@ -1,4 +1,5 @@ import frappe +from frappe import qb from erpnext.stock.doctype.item.test_item import create_item @@ -103,3 +104,15 @@ class AccountsTestMixin: ) new_acc.save() setattr(self, acc.attribute_name, new_acc.name) + + def clear_old_entries(self): + doctype_list = [ + "GL Entry", + "Payment Ledger Entry", + "Sales Invoice", + "Purchase Invoice", + "Payment Entry", + "Journal Entry", + ] + for doctype in doctype_list: + qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run() From 7a381affce02e5aea2c5a9a38ebf2638e8128f9a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 3 May 2023 20:45:12 +0530 Subject: [PATCH 22/60] refactor: limit output to 50 in reconciliation tool --- .../payment_reconciliation.json | 16 ++++++++++++- .../payment_reconciliation.py | 2 ++ erpnext/accounts/utils.py | 23 +++++++++++++++++-- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json index 5f6c7034ed..b88791d3f9 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json @@ -27,8 +27,10 @@ "bank_cash_account", "cost_center", "sec_break1", + "invoice_name", "invoices", "column_break_15", + "payment_name", "payments", "sec_break2", "allocation" @@ -137,6 +139,7 @@ "label": "Minimum Invoice Amount" }, { + "default": "50", "description": "System will fetch all the entries if limit value is zero.", "fieldname": "invoice_limit", "fieldtype": "Int", @@ -167,6 +170,7 @@ "label": "Maximum Payment Amount" }, { + "default": "50", "description": "System will fetch all the entries if limit value is zero.", "fieldname": "payment_limit", "fieldtype": "Int", @@ -194,13 +198,23 @@ "label": "Default Advance Account", "mandatory_depends_on": "doc.party_type", "options": "Account" + }, + { + "fieldname": "invoice_name", + "fieldtype": "Data", + "label": "Filter on Invoice" + }, + { + "fieldname": "payment_name", + "fieldtype": "Data", + "label": "Filter on Payment" } ], "hide_toolbar": 1, "icon": "icon-resize-horizontal", "issingle": 1, "links": [], - "modified": "2023-06-09 13:02:48.718362", + "modified": "2023-08-15 05:35:50.109290", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation", diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 3a9e80a9d9..5cc159ff4a 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -226,6 +226,8 @@ class PaymentReconciliation(Document): min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None, max_outstanding=self.maximum_invoice_amount if self.maximum_invoice_amount else None, accounting_dimensions=self.accounting_dimension_filter_conditions, + limit=self.invoice_limit, + voucher_no=self.invoice_name, ) cr_dr_notes = ( diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index bccf6f10b6..9d6d0f91fb 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -908,7 +908,9 @@ def get_outstanding_invoices( min_outstanding=None, max_outstanding=None, accounting_dimensions=None, - vouchers=None, + vouchers=None, # list of dicts [{'voucher_type': '', 'voucher_no': ''}] for filtering + limit=None, # passed by reconciliation tool + voucher_no=None, # filter passed by reconciliation tool ): ple = qb.DocType("Payment Ledger Entry") @@ -941,6 +943,8 @@ def get_outstanding_invoices( max_outstanding=max_outstanding, get_invoices=True, accounting_dimensions=accounting_dimensions or [], + limit=limit, + voucher_no=voucher_no, ) for d in invoice_list: @@ -1678,12 +1682,13 @@ class QueryPaymentLedger(object): self.voucher_posting_date = [] self.min_outstanding = None self.max_outstanding = None + self.limit = self.voucher_no = None def reset(self): # clear filters self.vouchers.clear() self.common_filter.clear() - self.min_outstanding = self.max_outstanding = None + self.min_outstanding = self.max_outstanding = self.limit = None # clear result self.voucher_outstandings.clear() @@ -1697,6 +1702,7 @@ class QueryPaymentLedger(object): filter_on_voucher_no = [] filter_on_against_voucher_no = [] + if self.vouchers: voucher_types = set([x.voucher_type for x in self.vouchers]) voucher_nos = set([x.voucher_no for x in self.vouchers]) @@ -1707,6 +1713,10 @@ class QueryPaymentLedger(object): filter_on_against_voucher_no.append(ple.against_voucher_type.isin(voucher_types)) filter_on_against_voucher_no.append(ple.against_voucher_no.isin(voucher_nos)) + if self.voucher_no: + filter_on_voucher_no.append(ple.voucher_no.like(f"%{self.voucher_no}%")) + filter_on_against_voucher_no.append(ple.against_voucher_no.like(f"%{self.voucher_no}%")) + # build outstanding amount filter filter_on_outstanding_amount = [] if self.min_outstanding: @@ -1822,6 +1832,11 @@ class QueryPaymentLedger(object): ) ) + if self.limit: + self.cte_query_voucher_amount_and_outstanding = ( + self.cte_query_voucher_amount_and_outstanding.limit(self.limit) + ) + # execute SQL self.voucher_outstandings = self.cte_query_voucher_amount_and_outstanding.run(as_dict=True) @@ -1835,6 +1850,8 @@ class QueryPaymentLedger(object): get_payments=False, get_invoices=False, accounting_dimensions=None, + limit=None, + voucher_no=None, ): """ Fetch voucher amount and outstanding amount from Payment Ledger using Database CTE @@ -1856,6 +1873,8 @@ class QueryPaymentLedger(object): self.max_outstanding = max_outstanding self.get_payments = get_payments self.get_invoices = get_invoices + self.limit = limit + self.voucher_no = voucher_no self.query_for_outstanding() return self.voucher_outstandings From e48f8139ebdd5aa64e3b9139157948a7a17afb61 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 4 Jul 2023 08:40:47 +0530 Subject: [PATCH 23/60] refactor: trigger on value change --- .../payment_reconciliation/payment_reconciliation.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index 2adc1238b7..7b7ce7a892 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -163,6 +163,15 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo this.frm.refresh(); } + invoice_name() { + this.frm.trigger("get_unreconciled_entries"); + } + + payment_name() { + this.frm.trigger("get_unreconciled_entries"); + } + + clear_child_tables() { this.frm.clear_table("invoices"); this.frm.clear_table("payments"); From 52f609e67a30988e498cf03bc0da7fa946ab3f5d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 4 Jul 2023 08:49:30 +0530 Subject: [PATCH 24/60] refactor: filter on cr/dr notes --- .../payment_reconciliation.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 5cc159ff4a..a724dbfa75 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -5,6 +5,7 @@ import frappe from frappe import _, msgprint, qb from frappe.model.document import Document +from frappe.query_builder import Criterion from frappe.query_builder.custom import ConstantColumn from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today @@ -146,6 +147,15 @@ class PaymentReconciliation(Document): def get_return_invoices(self): voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" doc = qb.DocType(voucher_type) + + conditions = [] + conditions.append(doc.docstatus == 1) + conditions.append(doc[frappe.scrub(self.party_type)] == self.party) + conditions.append(doc.is_return == 1) + + if self.payment_name: + conditions.append(doc.name.like(f"%{self.payment_name}%")) + self.return_invoices = ( qb.from_(doc) .select( @@ -153,11 +163,7 @@ class PaymentReconciliation(Document): doc.name.as_("voucher_no"), doc.return_against, ) - .where( - (doc.docstatus == 1) - & (doc[frappe.scrub(self.party_type)] == self.party) - & (doc.is_return == 1) - ) + .where(Criterion.all(conditions)) .run(as_dict=True) ) From 86bac2cf52e6094755e8f21df871a7f56a4d53a5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 4 Jul 2023 09:02:05 +0530 Subject: [PATCH 25/60] refactor: filter on advance payments --- .../doctype/payment_reconciliation/payment_reconciliation.py | 3 +++ erpnext/controllers/accounts_controller.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index a724dbfa75..5f69c6720b 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -75,6 +75,9 @@ class PaymentReconciliation(Document): } ) + if self.payment_name: + condition.update({"name": self.payment_name}) + payment_entries = get_advance_payment_entries( self.party_type, self.party, diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index fbf97aab59..7e43dbcd20 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2396,6 +2396,9 @@ def get_common_query( q = q.select((payment_entry.target_exchange_rate).as_("exchange_rate")) if condition: + if condition.get("name", None): + q = q.where(payment_entry.name.like(f"%{condition.get('name')}%")) + q = q.where(payment_entry.company == condition["company"]) q = ( q.where(payment_entry.posting_date >= condition["from_payment_date"]) From d01f0f2e965d776b9b142e33559efe012b4734e5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 4 Jul 2023 09:06:08 +0530 Subject: [PATCH 26/60] refactor: filter for journal entries --- .../doctype/payment_reconciliation/payment_reconciliation.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 5f69c6720b..68e3da3e85 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -93,6 +93,9 @@ class PaymentReconciliation(Document): def get_jv_entries(self): condition = self.get_conditions() + if self.payment_name: + condition += f" and t1.name like '%%{self.payment_name}%%'" + if self.get("cost_center"): condition += f" and t2.cost_center = '{self.cost_center}' " From 991770ed4ab9f5b49f547c10605e8464a2e9cab3 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 21 Aug 2023 21:32:23 +0530 Subject: [PATCH 27/60] fix: incorrect gl balance on AR summary rpt on multi company setup --- .../accounts_receivable_summary.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index 3675e80b54..cffc87895e 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -55,7 +55,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): ) if self.filters.show_gl_balance: - gl_balance_map = get_gl_balance(self.filters.report_date) + gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company) for party, party_dict in self.party_total.items(): if party_dict.outstanding == 0: @@ -232,12 +232,12 @@ class AccountsReceivableSummary(ReceivablePayableReport): self.add_column(label="Total Amount Due", fieldname="total_due") -def get_gl_balance(report_date): +def get_gl_balance(report_date, company): return frappe._dict( frappe.db.get_all( "GL Entry", fields=["party", "sum(debit - credit)"], - filters={"posting_date": ("<=", report_date), "is_cancelled": 0}, + filters={"posting_date": ("<=", report_date), "is_cancelled": 0, "company": company}, group_by="party", as_list=1, ) From e93b92705147adad3ffc6d37e863b898cfe8378c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 22 Aug 2023 05:32:56 +0530 Subject: [PATCH 28/60] chore: clean up stale code in reconciliation tool --- .../payment_reconciliation.py | 63 ------------------- 1 file changed, 63 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 3a9e80a9d9..0c62ba96ac 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -401,59 +401,6 @@ class PaymentReconciliation(Document): self.get_unreconciled_entries() - def make_difference_entry(self, row): - journal_entry = frappe.new_doc("Journal Entry") - journal_entry.voucher_type = "Exchange Gain Or Loss" - journal_entry.company = self.company - journal_entry.posting_date = nowdate() - journal_entry.multi_currency = 1 - - party_account_currency = frappe.get_cached_value( - "Account", self.receivable_payable_account, "account_currency" - ) - difference_account_currency = frappe.get_cached_value( - "Account", row.difference_account, "account_currency" - ) - - # Account Currency has balance - dr_or_cr = "debit" if self.party_type == "Customer" else "credit" - reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" - - journal_account = frappe._dict( - { - "account": self.receivable_payable_account, - "party_type": self.party_type, - "party": self.party, - "account_currency": party_account_currency, - "exchange_rate": 0, - "cost_center": erpnext.get_default_cost_center(self.company), - "reference_type": row.against_voucher_type, - "reference_name": row.against_voucher, - dr_or_cr: flt(row.difference_amount), - dr_or_cr + "_in_account_currency": 0, - } - ) - - journal_entry.append("accounts", journal_account) - - journal_account = frappe._dict( - { - "account": row.difference_account, - "account_currency": difference_account_currency, - "exchange_rate": 1, - "cost_center": erpnext.get_default_cost_center(self.company), - reverse_dr_or_cr + "_in_account_currency": flt(row.difference_amount), - reverse_dr_or_cr: flt(row.difference_amount), - } - ) - - journal_entry.append("accounts", journal_account) - - journal_entry.save() - journal_entry.submit() - - return journal_entry - def get_payment_details(self, row, dr_or_cr): return frappe._dict( { @@ -619,16 +566,6 @@ class PaymentReconciliation(Document): def reconcile_dr_cr_note(dr_cr_notes, company): - def get_difference_row(inv): - if inv.difference_amount != 0 and inv.difference_account: - difference_row = { - "account": inv.difference_account, - inv.dr_or_cr: abs(inv.difference_amount) if inv.difference_amount > 0 else 0, - reconcile_dr_or_cr: abs(inv.difference_amount) if inv.difference_amount < 0 else 0, - "cost_center": erpnext.get_default_cost_center(company), - } - return difference_row - for inv in dr_cr_notes: voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note" From 4b75b6f30940b943919196624dd1f904cc142c88 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Tue, 22 Aug 2023 08:56:01 +0200 Subject: [PATCH 29/60] feat(RFQ): optionally send document print (#36363) --- .../request_for_quotation.json | 11 ++++++++++- .../request_for_quotation.py | 15 ++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json index fbfc1ac169..06dbd86ba1 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -25,6 +25,7 @@ "col_break_email_1", "html_llwp", "send_attached_files", + "send_document_print", "sec_break_email_2", "message_for_supplier", "terms_section_break", @@ -283,13 +284,21 @@ "fieldname": "send_attached_files", "fieldtype": "Check", "label": "Send Attached Files" + }, + { + "default": "0", + "description": "If enabled, a print of this document will be attached to each email", + "fieldname": "send_document_print", + "fieldtype": "Check", + "label": "Send Document Print", + "print_hide": 1 } ], "icon": "fa fa-shopping-cart", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-08-08 16:30:10.870429", + "modified": "2023-08-09 12:20:26.850623", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation", diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 56840c11a6..8ea415df19 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -209,6 +209,20 @@ class RequestforQuotation(BuyingController): if self.send_attached_files: attachments = self.get_attachments() + if self.send_document_print: + supplier_language = frappe.db.get_value("Supplier", data.supplier, "language") + system_language = frappe.db.get_single_value("System Settings", "language") + attachments.append( + frappe.attach_print( + self.doctype, + self.name, + doc=self, + print_format=self.meta.default_print_format or "Standard", + lang=supplier_language or system_language, + letterhead=self.letter_head, + ) + ) + self.send_email(data, sender, subject, message, attachments) def send_email(self, data, sender, subject, message, attachments): @@ -218,7 +232,6 @@ class RequestforQuotation(BuyingController): recipients=data.email_id, sender=sender, attachments=attachments, - print_format=self.meta.default_print_format or "Standard", send_email=True, doctype=self.doctype, name=self.name, From 8f04945cef6e39db2ee41a63e1006ca180aba963 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Tue, 22 Aug 2023 12:36:09 +0530 Subject: [PATCH 30/60] fix: incorrect schedule in asset value adjustment (#36747) --- erpnext/assets/doctype/asset/asset.py | 2 +- erpnext/assets/doctype/asset/test_asset.py | 1 + .../asset_depreciation_schedule.py | 104 +++++++++++++++--- .../asset_value_adjustment.py | 99 +++++------------ .../test_asset_value_adjustment.py | 54 ++++++--- erpnext/controllers/buying_controller.py | 2 +- 6 files changed, 159 insertions(+), 103 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 2060c6ca83..ce894eb00d 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -56,7 +56,6 @@ class Asset(AccountsController): def on_submit(self): self.validate_in_use_date() - self.set_status() self.make_asset_movement() if not self.booked_fixed_asset and self.validate_make_gl_entry(): self.make_gl_entries() @@ -72,6 +71,7 @@ class Asset(AccountsController): "Asset Depreciation Schedules created:
{0}

Please check, edit if needed, and submit the Asset." ).format(asset_depr_schedules_links) ) + self.set_status() add_asset_activity(self.name, _("Asset submitted")) def on_cancel(self): diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index cd66f1d136..90eae2db38 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -19,6 +19,7 @@ from frappe.utils import ( from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.assets.doctype.asset.asset import ( + get_asset_value_after_depreciation, make_sales_invoice, split_asset, update_maintenance_status, diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py index 39ebd4ec0e..2b4b248a30 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -107,7 +107,7 @@ class AssetDepreciationSchedule(Document): have_asset_details_been_modified, not_manual_depr_or_have_manual_depr_details_been_modified ): self.make_depr_schedule(asset_doc, row, date_of_disposal, update_asset_finance_book_row) - self.set_accumulated_depreciation(row, date_of_disposal, date_of_return) + self.set_accumulated_depreciation(asset_doc, row, date_of_disposal, date_of_return) def have_asset_details_been_modified(self, asset_doc): return ( @@ -157,7 +157,12 @@ class AssetDepreciationSchedule(Document): self.status = "Draft" def make_depr_schedule( - self, asset_doc, row, date_of_disposal, update_asset_finance_book_row=True + self, + asset_doc, + row, + date_of_disposal, + update_asset_finance_book_row=True, + value_after_depreciation=None, ): if not self.get("depreciation_schedule"): self.depreciation_schedule = [] @@ -167,7 +172,9 @@ class AssetDepreciationSchedule(Document): start = self.clear_depr_schedule() - self._make_depr_schedule(asset_doc, row, start, date_of_disposal, update_asset_finance_book_row) + self._make_depr_schedule( + asset_doc, row, start, date_of_disposal, update_asset_finance_book_row, value_after_depreciation + ) def clear_depr_schedule(self): start = 0 @@ -187,23 +194,30 @@ class AssetDepreciationSchedule(Document): return start def _make_depr_schedule( - self, asset_doc, row, start, date_of_disposal, update_asset_finance_book_row + self, + asset_doc, + row, + start, + date_of_disposal, + update_asset_finance_book_row, + value_after_depreciation, ): asset_doc.validate_asset_finance_books(row) - value_after_depreciation = _get_value_after_depreciation_for_making_schedule(asset_doc, row) + if not value_after_depreciation: + value_after_depreciation = _get_value_after_depreciation_for_making_schedule(asset_doc, row) row.value_after_depreciation = value_after_depreciation if update_asset_finance_book_row: row.db_update() - number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint( + final_number_of_depreciations = cint(row.total_number_of_depreciations) - cint( self.number_of_depreciations_booked ) has_pro_rata = _check_is_pro_rata(asset_doc, row) if has_pro_rata: - number_of_pending_depreciations += 1 + final_number_of_depreciations += 1 has_wdv_or_dd_non_yearly_pro_rata = False if ( @@ -219,7 +233,9 @@ class AssetDepreciationSchedule(Document): depreciation_amount = 0 - for n in range(start, number_of_pending_depreciations): + number_of_pending_depreciations = final_number_of_depreciations - start + + for n in range(start, final_number_of_depreciations): # If depreciation is already completed (for double declining balance) if skip_row: continue @@ -236,10 +252,11 @@ class AssetDepreciationSchedule(Document): n, prev_depreciation_amount, has_wdv_or_dd_non_yearly_pro_rata, + number_of_pending_depreciations, ) if not has_pro_rata or ( - n < (cint(number_of_pending_depreciations) - 1) or number_of_pending_depreciations == 2 + n < (cint(final_number_of_depreciations) - 1) or final_number_of_depreciations == 2 ): schedule_date = add_months( row.depreciation_start_date, n * cint(row.frequency_of_depreciation) @@ -310,7 +327,7 @@ class AssetDepreciationSchedule(Document): ) # For last row - elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: + elif has_pro_rata and n == cint(final_number_of_depreciations) - 1: if not asset_doc.flags.increase_in_asset_life: # In case of increase_in_asset_life, the asset.to_date is already set on asset_repair submission asset_doc.to_date = add_months( @@ -343,7 +360,7 @@ class AssetDepreciationSchedule(Document): # Adjust depreciation amount in the last period based on the expected value after useful life if row.expected_value_after_useful_life and ( ( - n == cint(number_of_pending_depreciations) - 1 + n == cint(final_number_of_depreciations) - 1 and value_after_depreciation != row.expected_value_after_useful_life ) or value_after_depreciation < row.expected_value_after_useful_life @@ -392,6 +409,7 @@ class AssetDepreciationSchedule(Document): def set_accumulated_depreciation( self, + asset_doc, row, date_of_disposal=None, date_of_return=None, @@ -403,13 +421,21 @@ class AssetDepreciationSchedule(Document): if self.depreciation_method == "Straight Line" or self.depreciation_method == "Manual" ] - accumulated_depreciation = flt(self.opening_accumulated_depreciation) + accumulated_depreciation = None value_after_depreciation = flt(row.value_after_depreciation) for i, d in enumerate(self.get("depreciation_schedule")): if ignore_booked_entry and d.journal_entry: continue + if not accumulated_depreciation: + if i > 0 and asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment: + accumulated_depreciation = self.get("depreciation_schedule")[ + i - 1 + ].accumulated_depreciation_amount + else: + accumulated_depreciation = flt(self.opening_accumulated_depreciation) + depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount")) value_after_depreciation -= flt(depreciation_amount) @@ -507,9 +533,12 @@ def get_depreciation_amount( schedule_idx=0, prev_depreciation_amount=0, has_wdv_or_dd_non_yearly_pro_rata=False, + number_of_pending_depreciations=0, ): if fb_row.depreciation_method in ("Straight Line", "Manual"): - return get_straight_line_or_manual_depr_amount(asset, fb_row, schedule_idx) + return get_straight_line_or_manual_depr_amount( + asset, fb_row, schedule_idx, number_of_pending_depreciations + ) else: rate_of_depreciation = get_updated_rate_of_depreciation_for_wdv_and_dd( asset, depreciable_value, fb_row @@ -529,7 +558,9 @@ def get_updated_rate_of_depreciation_for_wdv_and_dd(asset, depreciable_value, fb return fb_row.rate_of_depreciation -def get_straight_line_or_manual_depr_amount(asset, row, schedule_idx): +def get_straight_line_or_manual_depr_amount( + asset, row, schedule_idx, number_of_pending_depreciations +): # if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value if asset.flags.increase_in_asset_life: return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / ( @@ -540,6 +571,36 @@ def get_straight_line_or_manual_depr_amount(asset, row, schedule_idx): return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / flt( row.total_number_of_depreciations ) + # if the Depreciation Schedule is being modified after Asset Value Adjustment due to decrease in asset value + elif asset.flags.decrease_in_asset_value_due_to_value_adjustment: + if row.daily_depreciation: + daily_depr_amount = ( + flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) + ) / date_diff( + add_months( + row.depreciation_start_date, + flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked) + * row.frequency_of_depreciation, + ), + add_months( + row.depreciation_start_date, + flt( + row.total_number_of_depreciations + - asset.number_of_depreciations_booked + - number_of_pending_depreciations + ) + * row.frequency_of_depreciation, + ), + ) + to_date = add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation) + from_date = add_months( + row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation + ) + return daily_depr_amount * date_diff(to_date, from_date) + else: + return ( + flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) + ) / number_of_pending_depreciations # if the Depreciation Schedule is being prepared for the first time else: if row.daily_depreciation: @@ -669,7 +730,12 @@ def cancel_asset_depr_schedules(asset_doc): def make_new_active_asset_depr_schedules_and_cancel_current_ones( - asset_doc, notes, date_of_disposal=None, date_of_return=None + asset_doc, + notes, + date_of_disposal=None, + date_of_return=None, + value_after_depreciation=None, + ignore_booked_entry=False, ): for row in asset_doc.get("finance_books"): current_asset_depr_schedule_doc = get_asset_depr_schedule_doc( @@ -695,8 +761,12 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones( row.rate_of_depreciation = new_rate_of_depreciation new_asset_depr_schedule_doc.rate_of_depreciation = new_rate_of_depreciation - new_asset_depr_schedule_doc.make_depr_schedule(asset_doc, row, date_of_disposal) - new_asset_depr_schedule_doc.set_accumulated_depreciation(row, date_of_disposal, date_of_return) + new_asset_depr_schedule_doc.make_depr_schedule( + asset_doc, row, date_of_disposal, value_after_depreciation=value_after_depreciation + ) + new_asset_depr_schedule_doc.set_accumulated_depreciation( + asset_doc, row, date_of_disposal, date_of_return, ignore_booked_entry + ) new_asset_depr_schedule_doc.notes = notes diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 823b6e9e21..9be7243602 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -5,7 +5,7 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import date_diff, flt, formatdate, get_link_to_form, getdate +from frappe.utils import flt, formatdate, get_link_to_form, getdate from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_checks_for_pl_and_bs_accounts, @@ -14,8 +14,7 @@ from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciatio from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( - get_asset_depr_schedule_doc, - get_depreciation_amount, + make_new_active_asset_depr_schedules_and_cancel_current_ones, ) @@ -27,7 +26,7 @@ class AssetValueAdjustment(Document): def on_submit(self): self.make_depreciation_entry() - self.reschedule_depreciations(self.new_asset_value) + self.update_asset(self.new_asset_value) add_asset_activity( self.asset, _("Asset's value adjusted after submission of Asset Value Adjustment {0}").format( @@ -36,7 +35,7 @@ class AssetValueAdjustment(Document): ) def on_cancel(self): - self.reschedule_depreciations(self.current_asset_value) + self.update_asset(self.current_asset_value) add_asset_activity( self.asset, _("Asset's value adjusted after cancellation of Asset Value Adjustment {0}").format( @@ -124,73 +123,33 @@ class AssetValueAdjustment(Document): self.db_set("journal_entry", je.name) - def reschedule_depreciations(self, asset_value): + def update_asset(self, asset_value): asset = frappe.get_doc("Asset", self.asset) - country = frappe.get_value("Company", self.company, "country") - for d in asset.finance_books: - d.value_after_depreciation = asset_value + if not asset.calculate_depreciation: + asset.value_after_depreciation = asset_value + asset.save() + return - current_asset_depr_schedule_doc = get_asset_depr_schedule_doc( - asset.name, "Active", d.finance_book + asset.flags.decrease_in_asset_value_due_to_value_adjustment = True + + if self.docstatus == 1: + notes = _( + "This schedule was created when Asset {0} was adjusted through Asset Value Adjustment {1}." + ).format( + get_link_to_form("Asset", asset.name), + get_link_to_form(self.get("doctype"), self.get("name")), + ) + elif self.docstatus == 2: + notes = _( + "This schedule was created when Asset {0}'s Asset Value Adjustment {1} was cancelled." + ).format( + get_link_to_form("Asset", asset.name), + get_link_to_form(self.get("doctype"), self.get("name")), ) - new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) - new_asset_depr_schedule_doc.status = "Draft" - new_asset_depr_schedule_doc.docstatus = 0 - - current_asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True - current_asset_depr_schedule_doc.cancel() - - if self.docstatus == 1: - notes = _( - "This schedule was created when Asset {0} was adjusted through Asset Value Adjustment {1}." - ).format( - get_link_to_form(asset.doctype, asset.name), - get_link_to_form(self.get("doctype"), self.get("name")), - ) - elif self.docstatus == 2: - notes = _( - "This schedule was created when Asset {0}'s Asset Value Adjustment {1} was cancelled." - ).format( - get_link_to_form(asset.doctype, asset.name), - get_link_to_form(self.get("doctype"), self.get("name")), - ) - new_asset_depr_schedule_doc.notes = notes - - new_asset_depr_schedule_doc.insert() - - depr_schedule = new_asset_depr_schedule_doc.get("depreciation_schedule") - - if d.depreciation_method in ("Straight Line", "Manual"): - end_date = max(s.schedule_date for s in depr_schedule) - total_days = date_diff(end_date, self.date) - rate_per_day = flt(d.value_after_depreciation - d.expected_value_after_useful_life) / flt( - total_days - ) - from_date = self.date - else: - no_of_depreciations = len([s.name for s in depr_schedule if not s.journal_entry]) - - value_after_depreciation = d.value_after_depreciation - for data in depr_schedule: - if not data.journal_entry: - if d.depreciation_method in ("Straight Line", "Manual"): - days = date_diff(data.schedule_date, from_date) - depreciation_amount = days * rate_per_day - from_date = data.schedule_date - else: - depreciation_amount = get_depreciation_amount(asset, value_after_depreciation, d) - - if depreciation_amount: - value_after_depreciation -= flt(depreciation_amount) - data.depreciation_amount = depreciation_amount - - d.db_update() - - new_asset_depr_schedule_doc.set_accumulated_depreciation(d, ignore_booked_entry=True) - for asset_data in depr_schedule: - if not asset_data.journal_entry: - asset_data.db_update() - - new_asset_depr_schedule_doc.submit() + make_new_active_asset_depr_schedules_and_cancel_current_ones( + asset, notes, value_after_depreciation=asset_value, ignore_booked_entry=True + ) + asset.flags.ignore_validate_update_after_submit = True + asset.save() diff --git a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py index 0b3dcba024..5d49759727 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py @@ -4,9 +4,10 @@ import unittest import frappe -from frappe.utils import add_days, get_last_day, nowdate +from frappe.utils import add_days, cstr, get_last_day, getdate, nowdate from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation +from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries from erpnext.assets.doctype.asset.test_asset import create_asset_data from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( get_asset_depr_schedule_doc, @@ -49,27 +50,23 @@ class TestAssetValueAdjustment(unittest.TestCase): def test_asset_depreciation_value_adjustment(self): pr = make_purchase_receipt( - item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location" + item_code="Macbook Pro", qty=1, rate=120000.0, location="Test Location" ) asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name") asset_doc = frappe.get_doc("Asset", asset_name) asset_doc.calculate_depreciation = 1 + asset_doc.available_for_use_date = "2023-01-15" + asset_doc.purchase_date = "2023-01-15" - month_end_date = get_last_day(nowdate()) - purchase_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15) - - asset_doc.available_for_use_date = purchase_date - asset_doc.purchase_date = purchase_date - asset_doc.calculate_depreciation = 1 asset_doc.append( "finance_books", { "expected_value_after_useful_life": 200, "depreciation_method": "Straight Line", - "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": month_end_date, + "total_number_of_depreciations": 12, + "frequency_of_depreciation": 1, + "depreciation_start_date": "2023-01-31", }, ) asset_doc.submit() @@ -77,9 +74,15 @@ class TestAssetValueAdjustment(unittest.TestCase): first_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active") self.assertEquals(first_asset_depr_schedule.status, "Active") + post_depreciation_entries(getdate("2023-08-21")) + current_value = get_asset_value_after_depreciation(asset_doc.name) + adj_doc = make_asset_value_adjustment( - asset=asset_doc.name, current_asset_value=current_value, new_asset_value=50000.0 + asset=asset_doc.name, + current_asset_value=current_value, + new_asset_value=50000.0, + date="2023-08-21", ) adj_doc.submit() @@ -90,8 +93,8 @@ class TestAssetValueAdjustment(unittest.TestCase): self.assertEquals(first_asset_depr_schedule.status, "Cancelled") expected_gle = ( - ("_Test Accumulated Depreciations - _TC", 0.0, 50000.0), - ("_Test Depreciations - _TC", 50000.0, 0.0), + ("_Test Accumulated Depreciations - _TC", 0.0, 4625.29), + ("_Test Depreciations - _TC", 4625.29, 0.0), ) gle = frappe.db.sql( @@ -103,6 +106,29 @@ class TestAssetValueAdjustment(unittest.TestCase): self.assertSequenceEqual(gle, expected_gle) + expected_schedules = [ + ["2023-01-31", 5474.73, 5474.73], + ["2023-02-28", 9983.33, 15458.06], + ["2023-03-31", 9983.33, 25441.39], + ["2023-04-30", 9983.33, 35424.72], + ["2023-05-31", 9983.33, 45408.05], + ["2023-06-30", 9983.33, 55391.38], + ["2023-07-31", 9983.33, 65374.71], + ["2023-08-31", 8300.0, 73674.71], + ["2023-09-30", 8300.0, 81974.71], + ["2023-10-31", 8300.0, 90274.71], + ["2023-11-30", 8300.0, 98574.71], + ["2023-12-31", 8300.0, 106874.71], + ["2024-01-15", 8300.0, 115174.71], + ] + + schedules = [ + [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] + for d in second_asset_depr_schedule.get("depreciation_schedule") + ] + + self.assertEqual(schedules, expected_schedules) + def make_asset_value_adjustment(**args): args = frappe._dict(args) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 7b7c53ecfe..b396b27da7 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -759,7 +759,7 @@ class BuyingController(SubcontractingController): "company": self.company, "supplier": self.supplier, "purchase_date": self.posting_date, - "calculate_depreciation": 1, + "calculate_depreciation": 0, "purchase_receipt_amount": purchase_amount, "gross_purchase_amount": purchase_amount, "asset_quantity": row.qty if is_grouped_asset else 0, From 9d29ec8eac9b0266bd8c5c5afd5415127e09b68d Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Tue, 22 Aug 2023 10:05:12 +0200 Subject: [PATCH 31/60] fix: attachments should be an empty list by default (#36757) fix: attachments should be an empty list by default --- .../doctype/request_for_quotation/request_for_quotation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 8ea415df19..6b39982bb8 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -205,7 +205,7 @@ class RequestforQuotation(BuyingController): if preview: return {"message": message, "subject": subject} - attachments = None + attachments = [] if self.send_attached_files: attachments = self.get_attachments() From 9349bc77c58aedf36e80739412f5786b40c97c19 Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Tue, 22 Aug 2023 14:12:42 +0530 Subject: [PATCH 32/60] fix: Accounts Payable Currency bug --- .../report/accounts_receivable/accounts_receivable.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index a7b35a579a..751063ad8e 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -214,8 +214,8 @@ class ReceivablePayableReport(object): for party_type in self.party_type: if self.filters.get(scrub(party_type)): amount = ple.amount_in_account_currency - else: - amount = ple.amount + else: + amount = ple.amount amount_in_account_currency = ple.amount_in_account_currency # update voucher From 87e2309e8e841a97aef06e854151a6ed6d3d32fc Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Tue, 22 Aug 2023 15:30:04 +0530 Subject: [PATCH 33/60] fix: avoid duplicate asset activity entries on asset capitalization (#36756) --- erpnext/assets/doctype/asset/asset.py | 13 ++++++++----- .../asset_capitalization/asset_capitalization.py | 1 + 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index ce894eb00d..ddb09c1f44 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -96,11 +96,14 @@ class Asset(AccountsController): "Asset Depreciation Schedules created:
{0}

Please check, edit if needed, and submit the Asset." ).format(asset_depr_schedules_links) ) - if not frappe.db.exists( - { - "doctype": "Asset Activity", - "asset": self.name, - } + if ( + not frappe.db.exists( + { + "doctype": "Asset Activity", + "asset": self.name, + } + ) + and not self.flags.asset_created_via_asset_capitalization ): add_asset_activity(self.name, _("Asset created")) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 324b7392a8..0bf2fbb14b 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -509,6 +509,7 @@ class AssetCapitalization(StockController): asset_doc.gross_purchase_amount = total_target_asset_value asset_doc.purchase_receipt_amount = total_target_asset_value asset_doc.flags.ignore_validate = True + asset_doc.flags.asset_created_via_asset_capitalization = True asset_doc.insert() self.target_asset = asset_doc.name From 9588bb74439de0c17cd0b30461bb3eadbb0b798e Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 21 Aug 2023 22:23:57 +0530 Subject: [PATCH 34/60] fix: validate FG Item and Qty --- .../doctype/purchase_order/purchase_order.py | 23 ++++++------ erpnext/controllers/accounts_controller.py | 35 +++++++++++++++++++ erpnext/public/js/utils.js | 4 ++- 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 4da1e76c2d..3576cd426d 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -49,18 +49,6 @@ class PurchaseOrder(BuyingController): } ] - def can_update_items(self) -> bool: - result = True - - if self.is_subcontracted and not self.is_old_subcontracting_flow: - # Check - 1: NOT ALLOWED if non-cancelled Subcontracting Order exists for this Purchase Order - if frappe.db.exists( - "Subcontracting Order", {"purchase_order": self.name, "docstatus": ["!=", 2]} - ): - return False - - return result - def onload(self): supplier_tds = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category") self.set_onload("supplier_tds", supplier_tds) @@ -463,6 +451,17 @@ class PurchaseOrder(BuyingController): else: self.db_set("per_received", 0, update_modified=False) + def can_update_items(self) -> bool: + result = True + + if self.is_subcontracted and not self.is_old_subcontracting_flow: + if frappe.db.exists( + "Subcontracting Order", {"purchase_order": self.name, "docstatus": ["!=", 2]} + ): + result = False + + return result + def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0): """get last purchase rate for an item""" diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 340ec01bee..081abe1210 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2855,6 +2855,27 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil return update_supplied_items + def validate_fg_item_for_subcontracting(new_data, is_new): + if is_new: + if not new_data.get("fg_item"): + frappe.throw( + _("Finished Good Item is not specified for service item {0}").format(new_data["item_code"]) + ) + else: + is_sub_contracted_item, default_bom = frappe.db.get_value( + "Item", new_data["fg_item"], ["is_sub_contracted_item", "default_bom"] + ) + + if not is_sub_contracted_item: + frappe.throw( + _("Finished Good Item {0} must be a sub-contracted item").format(new_data["fg_item"]) + ) + elif not default_bom: + frappe.throw(_("Default BOM not found for FG Item {0}").format(new_data["fg_item"])) + + if not new_data.get("fg_item_qty"): + frappe.throw(_("Finished Good Item {0} Qty can not be zero").format(new_data["fg_item"])) + data = json.loads(trans_items) any_qty_changed = False # updated to true if any item's qty changes @@ -2886,6 +2907,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil prev_rate, new_rate = flt(child_item.get("rate")), flt(d.get("rate")) prev_qty, new_qty = flt(child_item.get("qty")), flt(d.get("qty")) + prev_fg_qty, new_fg_qty = flt(child_item.get("fg_item_qty")), flt(d.get("fg_item_qty")) prev_con_fac, new_con_fac = flt(child_item.get("conversion_factor")), flt( d.get("conversion_factor") ) @@ -2898,6 +2920,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil rate_unchanged = prev_rate == new_rate qty_unchanged = prev_qty == new_qty + fg_qty_unchanged = prev_fg_qty == new_fg_qty uom_unchanged = prev_uom == new_uom conversion_factor_unchanged = prev_con_fac == new_con_fac any_conversion_factor_changed |= not conversion_factor_unchanged @@ -2907,6 +2930,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil if ( rate_unchanged and qty_unchanged + and fg_qty_unchanged and conversion_factor_unchanged and uom_unchanged and date_unchanged @@ -2917,6 +2941,17 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil if flt(child_item.get("qty")) != flt(d.get("qty")): any_qty_changed = True + if ( + parent.doctype == "Purchase Order" + and parent.is_subcontracted + and not parent.is_old_subcontracting_flow + ): + validate_fg_item_for_subcontracting(d, new_child_flag) + child_item.fg_item_qty = flt(d["fg_item_qty"]) + + if new_child_flag: + child_item.fg_item = d["fg_item"] + child_item.qty = flt(d.get("qty")) rate_precision = child_item.precision("rate") or 2 conv_fac_precision = child_item.precision("conversion_factor") or 2 diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 9116fd1f03..c11d123982 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -579,7 +579,9 @@ erpnext.utils.update_child_items = function(opts) { "conversion_factor": d.conversion_factor, "qty": d.qty, "rate": d.rate, - "uom": d.uom + "uom": d.uom, + "fg_item": d.fg_item, + "fg_item_qty": d.fg_item_qty, } }); From b9b1717e96f2dbb78ff9816d41270ed5aae626bb Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 22 Aug 2023 16:53:54 +0530 Subject: [PATCH 35/60] fix: re-validate PO while updating items --- erpnext/controllers/accounts_controller.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 081abe1210..843103da9b 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -3055,11 +3055,20 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil parent.update_ordered_qty() parent.update_ordered_and_reserved_qty() parent.update_receiving_percentage() - if parent.is_old_subcontracting_flow: - if should_update_supplied_items(parent): - parent.update_reserved_qty_for_subcontract() - parent.create_raw_materials_supplied() - parent.save() + + if parent.is_subcontracted: + if parent.is_old_subcontracting_flow: + if should_update_supplied_items(parent): + parent.update_reserved_qty_for_subcontract() + parent.create_raw_materials_supplied() + parent.save() + else: + if not parent.can_update_items(): + frappe.throw( + _( + "Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}." + ).format(frappe.bold(parent.name)) + ) else: # Sales Order parent.validate_warehouse() parent.update_reserved_qty() From 611c2bf775673be1b6fe5aba313df60d371ccc18 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 17:36:56 +0530 Subject: [PATCH 36/60] fix: not able to make stock entry (backport #36759) (#36767) fix: not able to make stock entry (#36759) (cherry picked from commit 873ee384a1f0832faf84015167f7bdc697f1dc46) Co-authored-by: rohitwaghchaure --- erpnext/stock/doctype/material_request/material_request.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 1139c4b83a..9efae6a9d6 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -656,7 +656,10 @@ def make_stock_entry(source_name, target_doc=None): "job_card_item": "job_card_item", }, "postprocess": update_item, - "condition": lambda doc: doc.ordered_qty < doc.stock_qty, + "condition": lambda doc: ( + flt(doc.ordered_qty, doc.precision("ordered_qty")) + < flt(doc.stock_qty, doc.precision("ordered_qty")) + ), }, }, target_doc, From 305d39f6a157abd285df523e4b1c4beb3e5af7d2 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 22 Aug 2023 18:24:30 +0530 Subject: [PATCH 37/60] test: add test case for Subcontract PO update items --- .../purchase_order/test_purchase_order.py | 65 +++++++++++++++++++ .../tests/test_subcontracting_controller.py | 2 +- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 3edaffae2a..55c01e8513 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -901,6 +901,71 @@ class TestPurchaseOrder(FrappeTestCase): self.assertRaises(frappe.ValidationError, po.save) + def test_update_items_for_subcontracting_purchase_order(self): + from erpnext.controllers.tests.test_subcontracting_controller import ( + get_subcontracting_order, + make_bom_for_subcontracted_items, + make_raw_materials, + make_service_items, + make_subcontracted_items, + ) + + def update_items(po, qty): + trans_items = [po.items[0].as_dict()] + trans_items[0]["qty"] = qty + trans_items[0]["fg_item_qty"] = qty + trans_items = json.dumps(trans_items, default=str) + + return update_child_qty_rate( + po.doctype, + trans_items, + po.name, + ) + + make_subcontracted_items() + make_raw_materials() + make_service_items() + make_bom_for_subcontracted_items() + + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 7", + "qty": 10, + "rate": 100, + "fg_item": "Subcontracted Item SA7", + "fg_item_qty": 10, + }, + ] + po = create_purchase_order( + rm_items=service_items, + is_subcontracted=1, + supplier_warehouse="_Test Warehouse 1 - _TC", + ) + + update_items(po, qty=20) + po.reload() + + # Test - 1: Items should be updated as there is no Subcontracting Order against PO + self.assertEqual(po.items[0].qty, 20) + self.assertEqual(po.items[0].fg_item_qty, 20) + + sco = get_subcontracting_order(po_name=po.name, warehouse="_Test Warehouse - _TC") + + # Test - 2: ValidationError should be raised as there is Subcontracting Order against PO + self.assertRaises(frappe.ValidationError, update_items, po=po, qty=30) + + sco.reload() + sco.cancel() + po.reload() + + update_items(po, qty=30) + po.reload() + + # Test - 3: Items should be updated as the Subcontracting Order is cancelled + self.assertEqual(po.items[0].qty, 30) + self.assertEqual(po.items[0].fg_item_qty, 30) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py index eeb35c4d96..6b61ae949d 100644 --- a/erpnext/controllers/tests/test_subcontracting_controller.py +++ b/erpnext/controllers/tests/test_subcontracting_controller.py @@ -1090,7 +1090,7 @@ def get_subcontracting_order(**args): po = frappe.get_doc("Purchase Order", args.get("po_name")) if po.is_subcontracted: - return create_subcontracting_order(po_name=po.name, **args) + return create_subcontracting_order(**args) if not args.service_items: service_items = [ From a77e9d36cc557293f4612cd1491b764613db0198 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 22:09:45 +0530 Subject: [PATCH 38/60] fix: Procurement Tracker report not showing material request items (backport #36768) (#36774) fix: Procurement Tracker report not showing material request items (#36768) (cherry picked from commit 6a9935c00e502a28fa976c963a218f7e11b891ec) Co-authored-by: rohitwaghchaure --- .../procurement_tracker.py | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/erpnext/buying/report/procurement_tracker/procurement_tracker.py b/erpnext/buying/report/procurement_tracker/procurement_tracker.py index 71019e8037..a7e03c08fa 100644 --- a/erpnext/buying/report/procurement_tracker/procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/procurement_tracker.py @@ -154,31 +154,35 @@ def get_data(filters): procurement_record = [] if procurement_record_against_mr: procurement_record += procurement_record_against_mr + for po in purchase_order_entry: # fetch material records linked to the purchase order item - mr_record = mr_records.get(po.material_request_item, [{}])[0] - procurement_detail = { - "material_request_date": mr_record.get("transaction_date"), - "cost_center": po.cost_center, - "project": po.project, - "requesting_site": po.warehouse, - "requestor": po.owner, - "material_request_no": po.material_request, - "item_code": po.item_code, - "quantity": flt(po.qty), - "unit_of_measurement": po.stock_uom, - "status": po.status, - "purchase_order_date": po.transaction_date, - "purchase_order": po.parent, - "supplier": po.supplier, - "estimated_cost": flt(mr_record.get("amount")), - "actual_cost": flt(pi_records.get(po.name)), - "purchase_order_amt": flt(po.amount), - "purchase_order_amt_in_company_currency": flt(po.base_amount), - "expected_delivery_date": po.schedule_date, - "actual_delivery_date": pr_records.get(po.name), - } - procurement_record.append(procurement_detail) + material_requests = mr_records.get(po.material_request_item, [{}]) + + for mr_record in material_requests: + procurement_detail = { + "material_request_date": mr_record.get("transaction_date"), + "cost_center": po.cost_center, + "project": po.project, + "requesting_site": po.warehouse, + "requestor": po.owner, + "material_request_no": po.material_request, + "item_code": po.item_code, + "quantity": flt(po.qty), + "unit_of_measurement": po.stock_uom, + "status": po.status, + "purchase_order_date": po.transaction_date, + "purchase_order": po.parent, + "supplier": po.supplier, + "estimated_cost": flt(mr_record.get("amount")), + "actual_cost": flt(pi_records.get(po.name)), + "purchase_order_amt": flt(po.amount), + "purchase_order_amt_in_company_currency": flt(po.base_amount), + "expected_delivery_date": po.schedule_date, + "actual_delivery_date": pr_records.get(po.name), + } + procurement_record.append(procurement_detail) + return procurement_record @@ -301,7 +305,7 @@ def get_po_entries(filters): & (parent.name == child.parent) & (parent.status.notin(("Closed", "Completed", "Cancelled"))) ) - .groupby(parent.name, child.item_code) + .groupby(parent.name, child.material_request_item) ) query = apply_filters_on_query(filters, parent, child, query) From 3fdcd33b922806038f1a61f9e94eb0f73c1304e6 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 23 Aug 2023 12:15:35 +0530 Subject: [PATCH 39/60] feat: `Quality Inspection` in `Subcontracting Receipt` --- erpnext/controllers/stock_controller.py | 1 + .../stock/doctype/quality_inspection/quality_inspection.json | 4 ++-- .../doctype/subcontracting_receipt/subcontracting_receipt.py | 3 +++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index d669abe910..ae54b801f1 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -599,6 +599,7 @@ class StockController(AccountsController): inspection_fieldname_map = { "Purchase Receipt": "inspection_required_before_purchase", "Purchase Invoice": "inspection_required_before_purchase", + "Subcontracting Receipt": "inspection_required_before_purchase", "Sales Invoice": "inspection_required_before_delivery", "Delivery Note": "inspection_required_before_delivery", } diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json index db9322f326..914a9f3c21 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json @@ -74,7 +74,7 @@ "fieldname": "reference_type", "fieldtype": "Select", "label": "Reference Type", - "options": "\nPurchase Receipt\nPurchase Invoice\nDelivery Note\nSales Invoice\nStock Entry\nJob Card", + "options": "\nPurchase Receipt\nPurchase Invoice\nSubcontracting Receipt\nDelivery Note\nSales Invoice\nStock Entry\nJob Card", "reqd": 1 }, { @@ -245,7 +245,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-10-04 22:00:13.995221", + "modified": "2023-08-23 11:56:50.282878", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 60746d95f3..58afc2f9aa 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -81,6 +81,9 @@ class SubcontractingReceipt(SubcontractingController): self.validate_posting_time() self.validate_rejected_warehouse() + if not self.get("is_return"): + self.validate_inspection() + if getdate(self.posting_date) > getdate(nowdate()): frappe.throw(_("Posting Date cannot be future date")) From 3fab6610cb2caf1778d201362b509c0f55541c71 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 23 Aug 2023 13:21:36 +0530 Subject: [PATCH 40/60] feat: setup Quality Inspection for Subcontracting Receipt - SCR[docstatus=0, is_return=0] Create > Quality Inspection(s) - Filters for Quality Inspection field in Subcontracting Receipt Items table --- erpnext/public/js/controllers/transaction.js | 4 +- .../subcontracting_receipt.js | 176 +++++++++--------- 2 files changed, 95 insertions(+), 85 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 59d2b154ac..3f16314781 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -277,7 +277,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } setup_quality_inspection() { - if(!in_list(["Delivery Note", "Sales Invoice", "Purchase Receipt", "Purchase Invoice"], this.frm.doc.doctype)) { + if(!in_list(["Delivery Note", "Sales Invoice", "Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"], this.frm.doc.doctype)) { return; } @@ -289,7 +289,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe this.frm.page.set_inner_btn_group_as_primary(__('Create')); } - const inspection_type = in_list(["Purchase Receipt", "Purchase Invoice"], this.frm.doc.doctype) + const inspection_type = in_list(["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"], this.frm.doc.doctype) ? "Incoming" : "Outgoing"; let quality_inspection_field = this.frm.get_docfield("items", "quality_inspection"); diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js index 94a2589b98..e374077a78 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -3,14 +3,91 @@ frappe.provide('erpnext.buying'); -erpnext.landed_cost_taxes_and_charges.setup_triggers("Subcontracting Receipt"); +erpnext.landed_cost_taxes_and_charges.setup_triggers('Subcontracting Receipt'); frappe.ui.form.on('Subcontracting Receipt', { setup: (frm) => { frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle']; frm.get_field('supplied_items').grid.cannot_add_rows = true; frm.get_field('supplied_items').grid.only_sortable(); + frm.trigger('set_queries'); + }, + refresh: (frm) => { + if (frm.doc.docstatus > 0) { + frm.add_custom_button(__('Stock Ledger'), () => { + frappe.route_options = { + voucher_no: frm.doc.name, + from_date: frm.doc.posting_date, + to_date: moment(frm.doc.modified).format('YYYY-MM-DD'), + company: frm.doc.company, + show_cancelled_entries: frm.doc.docstatus === 2 + }; + frappe.set_route('query-report', 'Stock Ledger'); + }, __('View')); + + frm.add_custom_button(__('Accounting Ledger'), () => { + frappe.route_options = { + voucher_no: frm.doc.name, + from_date: frm.doc.posting_date, + to_date: moment(frm.doc.modified).format('YYYY-MM-DD'), + company: frm.doc.company, + group_by: 'Group by Voucher (Consolidated)', + show_cancelled_entries: frm.doc.docstatus === 2 + }; + frappe.set_route('query-report', 'General Ledger'); + }, __('View')); + } + + if (!frm.doc.is_return && frm.doc.docstatus === 1 && frm.doc.per_returned < 100) { + frm.add_custom_button(__('Subcontract Return'), () => { + frappe.model.open_mapped_doc({ + method: 'erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return', + frm: frm + }); + }, __('Create')); + frm.page.set_inner_btn_group_as_primary(__('Create')); + } + + if (frm.doc.docstatus === 0) { + frm.add_custom_button(__('Subcontracting Order'), () => { + if (!frm.doc.supplier) { + frappe.throw({ + title: __('Mandatory'), + message: __('Please Select a Supplier') + }); + } + + erpnext.utils.map_current_doc({ + method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_subcontracting_receipt', + source_doctype: 'Subcontracting Order', + target: frm, + setters: { + supplier: frm.doc.supplier, + }, + get_query_filters: { + docstatus: 1, + per_received: ['<', 100], + company: frm.doc.company + } + }); + }, __('Get Items From')); + + frm.fields_dict.supplied_items.grid.update_docfield_property('consumed_qty', 'read_only', frm.doc.__onload && frm.doc.__onload.backflush_based_on === 'BOM'); + } + + frm.trigger('setup_quality_inspection'); + }, + + set_warehouse: (frm) => { + set_warehouse_in_children(frm.doc.items, 'warehouse', frm.doc.set_warehouse); + }, + + rejected_warehouse: (frm) => { + set_warehouse_in_children(frm.doc.items, 'rejected_warehouse', frm.doc.rejected_warehouse); + }, + + set_queries: (frm) => { frm.set_query('set_warehouse', () => { return { filters: { @@ -52,38 +129,36 @@ frappe.ui.form.on('Subcontracting Receipt', { } })); - frm.set_query('expense_account', 'items', function () { - return { + frm.set_query('expense_account', 'items', () => ({ query: 'erpnext.controllers.queries.get_expense_account', filters: { 'company': frm.doc.company } - }; - }); + })); - frm.set_query('batch_no', 'items', function(doc, cdt, cdn) { + frm.set_query('batch_no', 'items', (doc, cdt, cdn) => { var row = locals[cdt][cdn]; return { filters: { item: row.item_code } - } + }; }); - frm.set_query('batch_no', 'supplied_items', function(doc, cdt, cdn) { + frm.set_query('batch_no', 'supplied_items', (doc, cdt, cdn) => { var row = locals[cdt][cdn]; return { filters: { item: row.rm_item_code } - } + }; }); - frm.set_query("serial_and_batch_bundle", "supplied_items", (doc, cdt, cdn) => { + frm.set_query('serial_and_batch_bundle', 'supplied_items', (doc, cdt, cdn) => { let row = locals[cdt][cdn]; return { filters: { 'item_code': row.rm_item_code, 'voucher_type': doc.doctype, - 'voucher_no': ["in", [doc.name, ""]], + 'voucher_no': ['in', [doc.name, '']], 'is_cancelled': 0, } } @@ -101,7 +176,7 @@ frappe.ui.form.on('Subcontracting Receipt', { let batch_no_field = frm.get_docfield('items', 'batch_no'); if (batch_no_field) { - batch_no_field.get_route_options_for_new_doc = function(row) { + batch_no_field.get_route_options_for_new_doc = (row) => { return { 'item': row.doc.item_code } @@ -109,85 +184,20 @@ frappe.ui.form.on('Subcontracting Receipt', { } }, - refresh: (frm) => { - if (frm.doc.docstatus > 0) { - frm.add_custom_button(__('Stock Ledger'), function () { - frappe.route_options = { - voucher_no: frm.doc.name, - from_date: frm.doc.posting_date, - to_date: moment(frm.doc.modified).format('YYYY-MM-DD'), - company: frm.doc.company, - show_cancelled_entries: frm.doc.docstatus === 2 - }; - frappe.set_route('query-report', 'Stock Ledger'); - }, __('View')); - - frm.add_custom_button(__('Accounting Ledger'), function () { - frappe.route_options = { - voucher_no: frm.doc.name, - from_date: frm.doc.posting_date, - to_date: moment(frm.doc.modified).format('YYYY-MM-DD'), - company: frm.doc.company, - group_by: 'Group by Voucher (Consolidated)', - show_cancelled_entries: frm.doc.docstatus === 2 - }; - frappe.set_route('query-report', 'General Ledger'); - }, __('View')); + setup_quality_inspection: (frm) => { + if (!frm.is_new() && frm.doc.docstatus === 0 && !frm.doc.is_return) { + let transaction_controller = new erpnext.TransactionController({ frm: frm }); + transaction_controller.setup_quality_inspection(); } - - if (!frm.doc.is_return && frm.doc.docstatus == 1 && frm.doc.per_returned < 100) { - frm.add_custom_button(__('Subcontract Return'), function () { - frappe.model.open_mapped_doc({ - method: 'erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return', - frm: frm - }); - }, __('Create')); - frm.page.set_inner_btn_group_as_primary(__('Create')); - } - - if (frm.doc.docstatus == 0) { - frm.add_custom_button(__('Subcontracting Order'), function () { - if (!frm.doc.supplier) { - frappe.throw({ - title: __('Mandatory'), - message: __('Please Select a Supplier') - }); - } - - erpnext.utils.map_current_doc({ - method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_subcontracting_receipt', - source_doctype: 'Subcontracting Order', - target: frm, - setters: { - supplier: frm.doc.supplier, - }, - get_query_filters: { - docstatus: 1, - per_received: ['<', 100], - company: frm.doc.company - } - }); - }, __('Get Items From')); - - frm.fields_dict.supplied_items.grid.update_docfield_property('consumed_qty', 'read_only', frm.doc.__onload && frm.doc.__onload.backflush_based_on === 'BOM'); - } - }, - - set_warehouse: (frm) => { - set_warehouse_in_children(frm.doc.items, 'warehouse', frm.doc.set_warehouse); - }, - - rejected_warehouse: (frm) => { - set_warehouse_in_children(frm.doc.items, 'rejected_warehouse', frm.doc.rejected_warehouse); }, }); frappe.ui.form.on('Landed Cost Taxes and Charges', { - amount: function (frm, cdt, cdn) { + amount: (frm, cdt, cdn) => { frm.events.set_base_amount(frm, cdt, cdn); }, - expense_account: function (frm, cdt, cdn) { + expense_account: (frm, cdt, cdn) => { frm.events.set_account_currency(frm, cdt, cdn); } }); From c9ae9df9026c0a3d49cc59310361c4c7c879a672 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 23 Aug 2023 13:34:42 +0530 Subject: [PATCH 41/60] fix(ux): increase `Quality Inspection` dialog width --- erpnext/public/js/controllers/transaction.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 3f16314781..ac5735b707 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2067,6 +2067,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe const me = this; const dialog = new frappe.ui.Dialog({ title: __("Select Items for Quality Inspection"), + size: "extra-large", fields: fields, primary_action: function () { const data = dialog.get_values(); From 60eee564bfaa473ecb1afcf16d6e6f65b9928e34 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 23 Aug 2023 17:58:15 +0530 Subject: [PATCH 42/60] refactor: Payment btn criteria for Cr/Dr notes --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js | 3 +-- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 66438a7efa..efe97415a5 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -86,8 +86,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. } } - if(doc.docstatus == 1 && doc.outstanding_amount != 0 - && !(doc.is_return && doc.return_against) && !doc.on_hold) { + if(doc.docstatus == 1 && doc.outstanding_amount != 0 && !doc.on_hold) { this.frm.add_custom_button( __('Payment'), () => this.make_payment_entry(), diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index a4bcdb41db..642e99cd58 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -98,8 +98,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm); } - if (doc.docstatus == 1 && doc.outstanding_amount!=0 - && !(cint(doc.is_return) && doc.return_against)) { + if (doc.docstatus == 1 && doc.outstanding_amount!=0) { this.frm.add_custom_button( __('Payment'), () => this.make_payment_entry(), From 56b26852f31bf61d149953cc89134ca9441cc2d0 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Wed, 23 Aug 2023 18:01:59 +0530 Subject: [PATCH 43/60] fix: use current asset depr schedule to make temp schedule (#36783) fix: use current depr schedule to make temp schedule --- .../asset_depreciation_schedule.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py index 2b4b248a30..83350aaf0c 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -779,9 +779,20 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones( def get_temp_asset_depr_schedule_doc( asset_doc, row, date_of_disposal=None, date_of_return=None, update_asset_finance_book_row=False ): - asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule") + current_asset_depr_schedule_doc = get_asset_depr_schedule_doc( + asset_doc.name, "Active", row.finance_book + ) - asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data( + if not current_asset_depr_schedule_doc: + frappe.throw( + _("Asset Depreciation Schedule not found for Asset {0} and Finance Book {1}").format( + asset_doc.name, row.finance_book + ) + ) + + temp_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) + + temp_asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data( asset_doc, row, date_of_disposal, @@ -789,7 +800,7 @@ def get_temp_asset_depr_schedule_doc( update_asset_finance_book_row, ) - return asset_depr_schedule_doc + return temp_asset_depr_schedule_doc @frappe.whitelist() From bb3bd02f530f44bc99cc3af6b6ff4634b8ca7a40 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Wed, 23 Aug 2023 19:22:25 +0530 Subject: [PATCH 44/60] chore: styling improvements for asset depr sch table (#36792) * chore: improve asset depr sch table * chore: fix text color --- erpnext/assets/doctype/asset/asset.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 0a2f61d23b..962292b8ee 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -228,15 +228,19 @@ frappe.ui.form.on('Asset', { {name: __("Schedule Date"), editable: false, resizable: false, width: 270}, {name: __("Depreciation Amount"), editable: false, resizable: false, width: 164}, {name: __("Accumulated Depreciation Amount"), editable: false, resizable: false, width: 164}, - {name: __("Journal Entry"), editable: false, resizable: false, format: value => `${value}`, width: 312} + {name: __("Journal Entry"), editable: false, resizable: false, format: value => `${value}`, width: 304} ], data: data, + layout: "fluid", serialNoColumn: false, checkboxColumn: true, cellHeight: 35 }); - datatable.style.setStyle(`.dt-scrollable`, {'font-size': '0.75rem', 'margin-bottom': '1rem'}); + datatable.style.setStyle(`.dt-scrollable`, {'font-size': '0.75rem', 'margin-bottom': '1rem', 'margin-left': '0.35rem', 'margin-right': '0.35rem'}); + datatable.style.setStyle(`.dt-header`, {'margin-left': '0.35rem', 'margin-right': '0.35rem'}); + datatable.style.setStyle(`.dt-cell--header`, {'color': 'var(--text-muted)'}); + datatable.style.setStyle(`.dt-cell`, {'color': 'var(--text-color)'}); datatable.style.setStyle(`.dt-cell--col-1`, {'text-align': 'center'}); datatable.style.setStyle(`.dt-cell--col-2`, {'font-weight': 600}); datatable.style.setStyle(`.dt-cell--col-3`, {'font-weight': 600}); From 54ffe41b54d1a2246d338668300a35dff6254665 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Thu, 24 Aug 2023 06:42:18 +0200 Subject: [PATCH 45/60] feat(MR): Project and Cost Center in Connections (#36794) --- .../doctype/material_request/material_request_dashboard.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/stock/doctype/material_request/material_request_dashboard.py b/erpnext/stock/doctype/material_request/material_request_dashboard.py index 2bba52a4e2..f91ea6a0bb 100644 --- a/erpnext/stock/doctype/material_request/material_request_dashboard.py +++ b/erpnext/stock/doctype/material_request/material_request_dashboard.py @@ -6,6 +6,8 @@ def get_data(): "fieldname": "material_request", "internal_links": { "Sales Order": ["items", "sales_order"], + "Project": ["items", "project"], + "Cost Center": ["items", "cost_center"], }, "transactions": [ { @@ -15,5 +17,6 @@ def get_data(): {"label": _("Stock"), "items": ["Stock Entry", "Purchase Receipt", "Pick List"]}, {"label": _("Manufacturing"), "items": ["Work Order"]}, {"label": _("Internal Transfer"), "items": ["Sales Order"]}, + {"label": _("Accounting Dimensions"), "items": ["Project", "Cost Center"]}, ], } From 723563c16797e77b0722048db18dfcf0c32535a6 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 24 Aug 2023 10:13:47 +0530 Subject: [PATCH 46/60] fix: SCR return status (#36793) --- .../subcontracting_receipt.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 60746d95f3..d2bf7e8f1d 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -270,17 +270,24 @@ class SubcontractingReceipt(SubcontractingController): status = "Draft" elif self.docstatus == 1: status = "Completed" + if self.is_return: status = "Return" - return_against = frappe.get_doc("Subcontracting Receipt", self.return_against) - return_against.run_method("update_status") elif self.per_returned == 100: status = "Return Issued" + elif self.docstatus == 2: status = "Cancelled" + if self.is_return: + frappe.get_doc("Subcontracting Receipt", self.return_against).update_status( + update_modified=update_modified + ) + if status: - frappe.db.set_value("Subcontracting Receipt", self.name, "status", status, update_modified) + frappe.db.set_value( + "Subcontracting Receipt", self.name, "status", status, update_modified=update_modified + ) def get_gl_entries(self, warehouse_account=None): from erpnext.accounts.general_ledger import process_gl_map From f31eb74234e48596803c0a25dc2b08a33717eee5 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 23 Aug 2023 15:32:13 +0530 Subject: [PATCH 47/60] test: add test case for SCR Quality Inspection --- .../test_subcontracting_receipt.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index 887cba5b25..a170527e2d 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -567,6 +567,64 @@ class TestSubcontractingReceipt(FrappeTestCase): self.assertEqual(rm_item.rate, 100) self.assertEqual(rm_item.amount, rm_item.consumed_qty * rm_item.rate) + def test_quality_inspection_for_subcontracting_receipt(self): + from erpnext.stock.doctype.quality_inspection.test_quality_inspection import ( + create_quality_inspection, + ) + + set_backflush_based_on("BOM") + fg_item = "Subcontracted Item SA1" + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 5, + "rate": 100, + "fg_item": fg_item, + "fg_item_qty": 5, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + scr1 = make_subcontracting_receipt(sco.name) + scr1.save() + + # Enable `Inspection Required before Purchase` in Item Master + frappe.db.set_value("Item", fg_item, "inspection_required_before_purchase", 1) + + # ValidationError should be raised as Quality Inspection is not created/linked + self.assertRaises(frappe.ValidationError, scr1.submit) + + qa = create_quality_inspection( + reference_type="Subcontracting Receipt", + reference_name=scr1.name, + inspection_type="Incoming", + item_code=fg_item, + ) + scr1.reload() + self.assertEqual(scr1.items[0].quality_inspection, qa.name) + + # SCR should be submitted successfully as Quality Inspection is set + scr1.submit() + qa.cancel() + scr1.reload() + scr1.cancel() + + scr2 = make_subcontracting_receipt(sco.name) + scr2.save() + + # Disable `Inspection Required before Purchase` in Item Master + frappe.db.set_value("Item", fg_item, "inspection_required_before_purchase", 0) + + # ValidationError should not be raised as `Inspection Required before Purchase` is disabled + scr2.submit() + def make_return_subcontracting_receipt(**args): args = frappe._dict(args) From 6d9cebfee94a3485b207bbd5d298a680c26addef Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Aug 2023 11:49:03 +0530 Subject: [PATCH 48/60] fix: Tax withholding reversal on Debit Notes --- .../tax_withholding_category/tax_withholding_category.py | 4 ++-- erpnext/controllers/sales_and_purchase_return.py | 2 ++ 2 files changed, 4 insertions(+), 2 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 954b4e7957..de2f9e7e0d 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -271,9 +271,9 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details ) else: - tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0 + tax_amount = net_total * tax_details.rate / 100 else: - tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0 + tax_amount = net_total * tax_details.rate / 100 # once tds is deducted, not need to add vouchers in the invoice voucher_wise_amount = {} diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 173e812dbd..165e17b2d7 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -345,6 +345,8 @@ def make_return_doc( elif doctype == "Purchase Invoice": # look for Print Heading "Debit Note" doc.select_print_heading = frappe.get_cached_value("Print Heading", _("Debit Note")) + if source.tax_withholding_category: + doc.set_onload("supplier_tds", source.tax_withholding_category) for tax in doc.get("taxes") or []: if tax.charge_type == "Actual": From ab6e600b9ed1ccb8c40a626ad6cd9906b4036eb4 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 24 Aug 2023 12:23:33 +0530 Subject: [PATCH 49/60] fix: demo data setup w/o territory (#36798) This can fail because it's translated. --- erpnext/selling/doctype/customer/customer.py | 8 ++++++++ erpnext/setup/demo_data/customer.json | 5 +---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 555db59b08..d351c3cc5b 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -15,6 +15,7 @@ from frappe.model.mapper import get_mapped_doc from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options from frappe.model.utils.rename_doc import update_linked_doctypes from frappe.utils import cint, cstr, flt, get_formatted_email, today +from frappe.utils.nestedset import get_root_of from frappe.utils.user import get_users_with_role from erpnext.accounts.party import ( # noqa @@ -80,6 +81,7 @@ class Customer(TransactionBase): validate_party_accounts(self) self.validate_credit_limit_on_change() self.set_loyalty_program() + self.set_territory_and_group() self.check_customer_group_change() self.validate_default_bank_account() self.validate_internal_customer() @@ -138,6 +140,12 @@ class Customer(TransactionBase): _("{0} is not a company bank account").format(frappe.bold(self.default_bank_account)) ) + def set_territory_and_group(self): + if not self.territory: + self.territory = get_root_of("Territory") + if not self.customer_group: + self.customer_group = get_root_of("Customer Group") + def validate_internal_customer(self): if not self.is_internal_customer: self.represents_company = "" diff --git a/erpnext/setup/demo_data/customer.json b/erpnext/setup/demo_data/customer.json index 1b47906eb6..5e77e78937 100644 --- a/erpnext/setup/demo_data/customer.json +++ b/erpnext/setup/demo_data/customer.json @@ -2,19 +2,16 @@ { "doctype": "Customer", "customer_group": "Demo Customer Group", - "territory": "All Territories", "customer_name": "Grant Plastics Ltd." }, { "doctype": "Customer", "customer_group": "Demo Customer Group", - "territory": "All Territories", "customer_name": "West View Software Ltd." }, { "doctype": "Customer", "customer_group": "Demo Customer Group", - "territory": "All Territories", "customer_name": "Palmer Productions Ltd." } -] \ No newline at end of file +] From 8f1e00906fde550f3c49111f0015f7fe6241e275 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 24 Aug 2023 12:28:11 +0530 Subject: [PATCH 50/60] fix: fetch JVs with no party selected in filters --- .../tax_withholding_details.py | 54 ++++++++----------- 1 file changed, 23 insertions(+), 31 deletions(-) diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py index 7d16661472..7191720c57 100644 --- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py @@ -257,7 +257,7 @@ def get_tds_docs(filters): } party = frappe.get_all(filters.get("party_type"), pluck="name") - query_filters.update({"against": ("in", party)}) + or_filters.update({"against": ("in", party), "voucher_type": "Journal Entry"}) if filters.get("party"): del query_filters["account"] @@ -294,7 +294,7 @@ def get_tds_docs(filters): if journal_entries: journal_entry_party_map = get_journal_entry_party_map(journal_entries) - get_doc_info(journal_entries, "Journal Entry", tax_category_map) + get_doc_info(journal_entries, "Journal Entry", tax_category_map, net_total_map) return ( tds_documents, @@ -309,7 +309,11 @@ def get_journal_entry_party_map(journal_entries): journal_entry_party_map = {} for d in frappe.db.get_all( "Journal Entry Account", - {"parent": ("in", journal_entries), "party_type": "Supplier", "party": ("is", "set")}, + { + "parent": ("in", journal_entries), + "party_type": ("in", ("Supplier", "Customer")), + "party": ("is", "set"), + }, ["parent", "party"], ): if d.parent not in journal_entry_party_map: @@ -320,41 +324,29 @@ def get_journal_entry_party_map(journal_entries): def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None): - if doctype == "Purchase Invoice": - fields = [ - "name", - "tax_withholding_category", - "base_tax_withholding_net_total", - "grand_total", - "base_total", - ] - elif doctype == "Sales Invoice": - fields = ["name", "base_net_total", "grand_total", "base_total"] - elif doctype == "Payment Entry": - fields = [ - "name", - "tax_withholding_category", - "paid_amount", - "paid_amount_after_tax", - "base_paid_amount", - ] - else: - fields = ["name", "tax_withholding_category"] + common_fields = ["name", "tax_withholding_category"] + fields_dict = { + "Purchase Invoice": ["base_tax_withholding_net_total", "grand_total", "base_total"], + "Sales Invoice": ["base_net_total", "grand_total", "base_total"], + "Payment Entry": ["paid_amount", "paid_amount_after_tax", "base_paid_amount"], + "Journal Entry": ["total_amount"], + } - entries = frappe.get_all(doctype, filters={"name": ("in", vouchers)}, fields=fields) + entries = frappe.get_all( + doctype, filters={"name": ("in", vouchers)}, fields=common_fields + fields_dict[doctype] + ) for entry in entries: tax_category_map.update({entry.name: entry.tax_withholding_category}) if doctype == "Purchase Invoice": - net_total_map.update( - {entry.name: [entry.base_tax_withholding_net_total, entry.grand_total, entry.base_total]} - ) + value = [entry.base_tax_withholding_net_total, entry.grand_total, entry.base_total] elif doctype == "Sales Invoice": - net_total_map.update({entry.name: [entry.base_net_total, entry.grand_total, entry.base_total]}) + value = [entry.base_net_total, entry.grand_total, entry.base_total] elif doctype == "Payment Entry": - net_total_map.update( - {entry.name: [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount]} - ) + value = [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount] + else: + value = [entry.total_amount] * 3 + net_total_map.update({entry.name: value}) def get_tax_rate_map(filters): From 7c1417e19913ded3424bb940f770480898a72771 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 24 Aug 2023 12:55:54 +0530 Subject: [PATCH 51/60] chore: fix linting issues --- erpnext/accounts/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 9d6d0f91fb..1aefeaacf7 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -908,9 +908,9 @@ def get_outstanding_invoices( min_outstanding=None, max_outstanding=None, accounting_dimensions=None, - vouchers=None, # list of dicts [{'voucher_type': '', 'voucher_no': ''}] for filtering - limit=None, # passed by reconciliation tool - voucher_no=None, # filter passed by reconciliation tool + vouchers=None, # list of dicts [{'voucher_type': '', 'voucher_no': ''}] for filtering + limit=None, # passed by reconciliation tool + voucher_no=None, # filter passed by reconciliation tool ): ple = qb.DocType("Payment Ledger Entry") From 3c15feadf66c1629e9bc373b28d435c7cc2b1825 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 24 Aug 2023 17:24:44 +0530 Subject: [PATCH 52/60] feat: Multi-level BOM Creator (#36494) * feat: Multi-level BOM Creator * fix: renamed BOM Configurator to BOM Creator * fix: added Cost in the tree * fix: finished good cost * fix: valuation rate in tree ui * chore: conflicts and removed unnecessary files * test: test cases for BOM Creator * fix: added shortcut for the BOM Creator * fix: added validation for Final Product --- erpnext/manufacturing/doctype/bom/bom.json | 34 +- erpnext/manufacturing/doctype/bom/bom.py | 50 ++- .../doctype/bom_creator/__init__.py | 0 .../doctype/bom_creator/bom_creator.js | 201 +++++++++ .../doctype/bom_creator/bom_creator.json | 330 ++++++++++++++ .../doctype/bom_creator/bom_creator.py | 424 ++++++++++++++++++ .../doctype/bom_creator/bom_creator_list.js | 18 + .../doctype/bom_creator/test_bom_creator.py | 240 ++++++++++ .../doctype/bom_creator_item/__init__.py | 0 .../bom_creator_item/bom_creator_item.json | 243 ++++++++++ .../bom_creator_item/bom_creator_item.py | 9 + .../manufacturing/manufacturing.json | 11 +- .../bom_configurator.bundle.js | 416 +++++++++++++++++ erpnext/public/js/utils.js | 4 + 14 files changed, 1963 insertions(+), 17 deletions(-) create mode 100644 erpnext/manufacturing/doctype/bom_creator/__init__.py create mode 100644 erpnext/manufacturing/doctype/bom_creator/bom_creator.js create mode 100644 erpnext/manufacturing/doctype/bom_creator/bom_creator.json create mode 100644 erpnext/manufacturing/doctype/bom_creator/bom_creator.py create mode 100644 erpnext/manufacturing/doctype/bom_creator/bom_creator_list.js create mode 100644 erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py create mode 100644 erpnext/manufacturing/doctype/bom_creator_item/__init__.py create mode 100644 erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json create mode 100644 erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py create mode 100644 erpnext/public/js/bom_configurator/bom_configurator.bundle.js diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index d02402299e..e8d3542835 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -78,6 +78,10 @@ "show_items", "show_operations", "web_long_description", + "reference_section", + "bom_creator", + "bom_creator_item", + "column_break_oxbz", "amended_from", "connections_tab" ], @@ -233,7 +237,7 @@ "fieldname": "rm_cost_as_per", "fieldtype": "Select", "label": "Rate Of Materials Based On", - "options": "Valuation Rate\nLast Purchase Rate\nPrice List" + "options": "Valuation Rate\nLast Purchase Rate\nPrice List\nManual" }, { "allow_on_submit": 1, @@ -599,6 +603,32 @@ "fieldname": "operating_cost_per_bom_quantity", "fieldtype": "Currency", "label": "Operating Cost Per BOM Quantity" + }, + { + "fieldname": "reference_section", + "fieldtype": "Section Break", + "label": "Reference" + }, + { + "fieldname": "bom_creator", + "fieldtype": "Link", + "label": "BOM Creator", + "no_copy": 1, + "options": "BOM Creator", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "bom_creator_item", + "fieldtype": "Data", + "label": "BOM Creator Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_oxbz", + "fieldtype": "Column Break" } ], "icon": "fa fa-sitemap", @@ -606,7 +636,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2023-04-06 12:47:58.514795", + "modified": "2023-08-07 11:38:08.152294", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 8058a5f8b7..023166849d 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -206,6 +206,7 @@ class BOM(WebsiteGenerator): def on_submit(self): self.manage_default_bom() + self.update_bom_creator_status() def on_cancel(self): self.db_set("is_active", 0) @@ -214,6 +215,23 @@ class BOM(WebsiteGenerator): # check if used in any other bom self.validate_bom_links() self.manage_default_bom() + self.update_bom_creator_status() + + def update_bom_creator_status(self): + if not self.bom_creator: + return + + if self.bom_creator_item: + frappe.db.set_value( + "BOM Creator Item", + self.bom_creator_item, + "bom_created", + 1 if self.docstatus == 1 else 0, + update_modified=False, + ) + + doc = frappe.get_doc("BOM Creator", self.bom_creator) + doc.set_status(save=True) def on_update_after_submit(self): self.validate_bom_links() @@ -662,18 +680,19 @@ class BOM(WebsiteGenerator): for d in self.get("items"): old_rate = d.rate - d.rate = self.get_rm_rate( - { - "company": self.company, - "item_code": d.item_code, - "bom_no": d.bom_no, - "qty": d.qty, - "uom": d.uom, - "stock_uom": d.stock_uom, - "conversion_factor": d.conversion_factor, - "sourced_by_supplier": d.sourced_by_supplier, - } - ) + if self.rm_cost_as_per != "Manual": + d.rate = self.get_rm_rate( + { + "company": self.company, + "item_code": d.item_code, + "bom_no": d.bom_no, + "qty": d.qty, + "uom": d.uom, + "stock_uom": d.stock_uom, + "conversion_factor": d.conversion_factor, + "sourced_by_supplier": d.sourced_by_supplier, + } + ) d.base_rate = flt(d.rate) * flt(self.conversion_rate) d.amount = flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty")) @@ -964,7 +983,12 @@ def get_valuation_rate(data): .as_("valuation_rate") ) .where((bin_table.item_code == item_code) & (wh_table.company == company)) - ).run(as_dict=True)[0] + ) + + if data.get("set_rate_based_on_warehouse") and data.get("warehouse"): + item_valuation = item_valuation.where(bin_table.warehouse == data.get("warehouse")) + + item_valuation = item_valuation.run(as_dict=True)[0] valuation_rate = item_valuation.get("valuation_rate") diff --git a/erpnext/manufacturing/doctype/bom_creator/__init__.py b/erpnext/manufacturing/doctype/bom_creator/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.js b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js new file mode 100644 index 0000000000..01dc89b080 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js @@ -0,0 +1,201 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +frappe.provide("erpnext.bom"); + +frappe.ui.form.on("BOM Creator", { + setup(frm) { + frm.trigger("set_queries"); + }, + + setup_bom_creator(frm) { + frm.dashboard.clear_comment(); + + if (!frm.is_new()) { + if ((!frappe.bom_configurator + || frappe.bom_configurator.bom_configurator !== frm.doc.name)) { + frm.trigger("build_tree"); + } + } else { + let $parent = $(frm.fields_dict["bom_creator"].wrapper); + $parent.empty(); + frm.trigger("make_new_entry"); + } + }, + + build_tree(frm) { + let $parent = $(frm.fields_dict["bom_creator"].wrapper); + $parent.empty(); + frm.toggle_enable("item_code", false); + + frappe.require('bom_configurator.bundle.js').then(() => { + frappe.bom_configurator = new frappe.ui.BOMConfigurator({ + wrapper: $parent, + page: $parent, + frm: frm, + bom_configurator: frm.doc.name, + }); + }); + }, + + make_new_entry(frm) { + let dialog = new frappe.ui.Dialog({ + title: __("Multi-level BOM Creator"), + fields: [ + { + label: __("Name"), + fieldtype: "Data", + fieldname: "name", + reqd: 1 + }, + { fieldtype: "Column Break" }, + { + label: __("Company"), + fieldtype: "Link", + fieldname: "company", + options: "Company", + reqd: 1, + default: frappe.defaults.get_user_default("Company"), + }, + { fieldtype: "Section Break" }, + { + label: __("Item Code (Final Product)"), + fieldtype: "Link", + fieldname: "item_code", + options: "Item", + reqd: 1 + }, + { fieldtype: "Column Break" }, + { + label: __("Quantity"), + fieldtype: "Float", + fieldname: "qty", + reqd: 1, + default: 1.0 + }, + { fieldtype: "Section Break" }, + { + label: __("Currency"), + fieldtype: "Link", + fieldname: "currency", + options: "Currency", + reqd: 1, + default: frappe.defaults.get_global_default("currency") + }, + { fieldtype: "Column Break" }, + { + label: __("Conversion Rate"), + fieldtype: "Float", + fieldname: "conversion_rate", + reqd: 1, + default: 1.0 + }, + ], + primary_action_label: __("Create"), + primary_action: (values) => { + values.doctype = frm.doc.doctype; + frappe.db + .insert(values) + .then((doc) => { + frappe.set_route("Form", doc.doctype, doc.name); + }); + } + }) + + dialog.show(); + }, + + set_queries(frm) { + frm.set_query("bom_no", "items", function(doc, cdt, cdn) { + let item = frappe.get_doc(cdt, cdn); + return { + filters: { + item: item.item_code, + } + } + }); + }, + + refresh(frm) { + frm.trigger("setup_bom_creator"); + frm.trigger("set_root_item"); + frm.trigger("add_custom_buttons"); + }, + + set_root_item(frm) { + if (frm.is_new() && frm.doc.items?.length) { + frappe.model.set_value(frm.doc.items[0].doctype, + frm.doc.items[0].name, "is_root", 1); + } + }, + + add_custom_buttons(frm) { + if (!frm.is_new()) { + frm.add_custom_button(__("Rebuild Tree"), () => { + frm.trigger("build_tree"); + }); + } + } +}); + +frappe.ui.form.on("BOM Creator Item", { + item_code(frm, cdt, cdn) { + let item = frappe.get_doc(cdt, cdn); + if (item.item_code && item.is_root) { + frappe.model.set_value(cdt, cdn, "fg_item", item.item_code); + } + }, + + do_not_explode(frm, cdt, cdn) { + let item = frappe.get_doc(cdt, cdn); + if (!item.do_not_explode) { + frm.call({ + method: "get_default_bom", + doc: frm.doc, + args: { + item_code: item.item_code + }, + callback(r) { + if (r.message) { + frappe.model.set_value(cdt, cdn, "bom_no", r.message); + } + } + }) + } else { + frappe.model.set_value(cdt, cdn, "bom_no", ""); + } + } +}); + + +erpnext.bom.BomConfigurator = class BomConfigurator extends erpnext.TransactionController { + conversion_rate(doc) { + if(this.frm.doc.currency === this.get_company_currency()) { + this.frm.set_value("conversion_rate", 1.0); + } else { + erpnext.bom.update_cost(doc); + } + } + + buying_price_list(doc) { + this.apply_price_list(); + } + + plc_conversion_rate(doc) { + if (!this.in_apply_price_list) { + this.apply_price_list(null, true); + } + } + + conversion_factor(doc, cdt, cdn) { + if (frappe.meta.get_docfield(cdt, "stock_qty", cdn)) { + var item = frappe.get_doc(cdt, cdn); + frappe.model.round_floats_in(item, ["qty", "conversion_factor"]); + item.stock_qty = flt(item.qty * item.conversion_factor, precision("stock_qty", item)); + refresh_field("stock_qty", item.name, item.parentfield); + this.toggle_conversion_factor(item); + this.frm.events.update_cost(this.frm); + } + } +}; + +extend_cscript(cur_frm.cscript, new erpnext.bom.BomConfigurator({frm: cur_frm})); \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.json b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json new file mode 100644 index 0000000000..fb4c6c5c95 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json @@ -0,0 +1,330 @@ +{ + "actions": [], + "allow_import": 1, + "autoname": "prompt", + "creation": "2023-07-18 14:56:34.477800", + "default_view": "List", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "tab_2_tab", + "bom_creator", + "details_tab", + "section_break_ylsl", + "item_code", + "item_name", + "item_group", + "column_break_ikj7", + "qty", + "project", + "uom", + "raw_materials_tab", + "currency_detail", + "rm_cost_as_per", + "set_rate_based_on_warehouse", + "buying_price_list", + "price_list_currency", + "plc_conversion_rate", + "column_break_ivyw", + "currency", + "conversion_rate", + "section_break_zcfg", + "default_warehouse", + "column_break_tzot", + "company", + "materials_section", + "items", + "costing_detail", + "raw_material_cost", + "remarks_tab", + "remarks", + "section_break_yixm", + "status", + "column_break_irab", + "error_log", + "connections_tab", + "amended_from" + ], + "fields": [ + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "remember_last_selected_value": 1, + "reqd": 1 + }, + { + "fieldname": "currency_detail", + "fieldtype": "Section Break", + "label": "Costing" + }, + { + "allow_on_submit": 1, + "default": "Valuation Rate", + "fieldname": "rm_cost_as_per", + "fieldtype": "Select", + "label": "Rate Of Materials Based On", + "options": "Valuation Rate\nLast Purchase Rate\nPrice List\nManual", + "reqd": 1 + }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.rm_cost_as_per===\"Price List\"", + "fieldname": "buying_price_list", + "fieldtype": "Link", + "label": "Price List", + "options": "Price List" + }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.rm_cost_as_per=='Price List'", + "fieldname": "price_list_currency", + "fieldtype": "Link", + "label": "Price List Currency", + "options": "Currency", + "print_hide": 1, + "read_only": 1 + }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.rm_cost_as_per=='Price List'", + "fieldname": "plc_conversion_rate", + "fieldtype": "Float", + "label": "Price List Exchange Rate" + }, + { + "fieldname": "column_break_ivyw", + "fieldtype": "Column Break" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Currency", + "options": "Currency", + "reqd": 1 + }, + { + "default": "1", + "fieldname": "conversion_rate", + "fieldtype": "Float", + "label": "Conversion Rate", + "precision": "9" + }, + { + "fieldname": "materials_section", + "fieldtype": "Section Break", + "oldfieldtype": "Section Break" + }, + { + "allow_bulk_edit": 1, + "fieldname": "items", + "fieldtype": "Table", + "label": "Items", + "oldfieldname": "bom_materials", + "oldfieldtype": "Table", + "options": "BOM Creator Item" + }, + { + "fieldname": "costing_detail", + "fieldtype": "Section Break", + "label": "Costing Details" + }, + { + "fieldname": "raw_material_cost", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Total Cost", + "no_copy": 1, + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "remarks", + "fieldtype": "Text Editor", + "label": "Remarks" + }, + { + "fieldname": "column_break_ikj7", + "fieldtype": "Column Break" + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + }, + { + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Finished Good", + "options": "Item", + "reqd": 1 + }, + { + "fieldname": "qty", + "fieldtype": "Float", + "label": "Quantity", + "reqd": 1 + }, + { + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name" + }, + { + "fetch_from": "item_code.stock_uom", + "fieldname": "uom", + "fieldtype": "Link", + "label": "UOM", + "options": "UOM" + }, + { + "fieldname": "tab_2_tab", + "fieldtype": "Tab Break", + "label": "BOM Tree" + }, + { + "fieldname": "details_tab", + "fieldtype": "Tab Break", + "label": "Final Product" + }, + { + "fieldname": "raw_materials_tab", + "fieldtype": "Tab Break", + "label": "Sub Assemblies & Raw Materials" + }, + { + "fieldname": "remarks_tab", + "fieldtype": "Tab Break", + "label": "Remarks" + }, + { + "fieldname": "connections_tab", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 + }, + { + "fetch_from": "item_code.item_group", + "fieldname": "item_group", + "fieldtype": "Link", + "label": "Item Group", + "options": "Item Group" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "BOM Creator", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "section_break_zcfg", + "fieldtype": "Section Break", + "label": "Warehouse" + }, + { + "fieldname": "column_break_tzot", + "fieldtype": "Column Break" + }, + { + "fieldname": "default_warehouse", + "fieldtype": "Link", + "label": "Default Source Warehouse", + "options": "Warehouse" + }, + { + "fieldname": "bom_creator", + "fieldtype": "HTML" + }, + { + "fieldname": "section_break_ylsl", + "fieldtype": "Section Break" + }, + { + "default": "0", + "depends_on": "eval:doc.rm_cost_as_per === \"Valuation Rate\"", + "fieldname": "set_rate_based_on_warehouse", + "fieldtype": "Check", + "label": "Set Valuation Rate Based on Source Warehouse" + }, + { + "fieldname": "section_break_yixm", + "fieldtype": "Section Break" + }, + { + "default": "Draft", + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "no_copy": 1, + "options": "Draft\nSubmitted\nIn Progress\nCompleted\nFailed\nCancelled", + "read_only": 1 + }, + { + "fieldname": "column_break_irab", + "fieldtype": "Column Break" + }, + { + "fieldname": "error_log", + "fieldtype": "Text", + "label": "Error Log", + "read_only": 1 + } + ], + "icon": "fa fa-sitemap", + "is_submittable": 1, + "links": [ + { + "link_doctype": "BOM", + "link_fieldname": "bom_creator" + } + ], + "modified": "2023-08-07 15:45:06.176313", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "BOM Creator", + "naming_rule": "Set by user", + "owner": "Administrator", + "permissions": [ + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing User", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py new file mode 100644 index 0000000000..999d610dfa --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py @@ -0,0 +1,424 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from collections import OrderedDict + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import flt + +from erpnext.manufacturing.doctype.bom.bom import get_bom_item_rate + +BOM_FIELDS = [ + "company", + "rm_cost_as_per", + "project", + "currency", + "conversion_rate", + "buying_price_list", +] + +BOM_ITEM_FIELDS = [ + "item_code", + "qty", + "uom", + "rate", + "stock_qty", + "stock_uom", + "conversion_factor", + "do_not_explode", +] + + +class BOMCreator(Document): + def before_save(self): + self.set_status() + self.set_is_expandable() + self.set_conversion_factor() + self.set_reference_id() + self.set_rate_for_items() + + def validate(self): + self.validate_items() + + def validate_items(self): + for row in self.items: + if row.is_expandable and row.item_code == self.item_code: + frappe.throw(_("Item {0} cannot be added as a sub-assembly of itself").format(row.item_code)) + + def set_status(self, save=False): + self.status = { + 0: "Draft", + 1: "Submitted", + 2: "Cancelled", + }[self.docstatus] + + self.set_status_completed() + if save: + self.db_set("status", self.status) + + def set_status_completed(self): + if self.docstatus != 1: + return + + has_completed = True + for row in self.items: + if row.is_expandable and not row.bom_created: + has_completed = False + break + + if not frappe.get_cached_value( + "BOM", {"bom_creator": self.name, "item": self.item_code}, "name" + ): + has_completed = False + + if has_completed: + self.status = "Completed" + + def on_cancel(self): + self.set_status(True) + + def set_conversion_factor(self): + for row in self.items: + row.conversion_factor = 1.0 + + def before_submit(self): + self.validate_fields() + self.set_status() + + def set_reference_id(self): + parent_reference = {row.idx: row.name for row in self.items} + + for row in self.items: + if row.fg_reference_id: + continue + + if row.parent_row_no: + row.fg_reference_id = parent_reference.get(row.parent_row_no) + + @frappe.whitelist() + def add_boms(self): + self.submit() + + def set_rate_for_items(self): + if self.rm_cost_as_per == "Manual": + return + + amount = self.get_raw_material_cost() + self.raw_material_cost = amount + + def get_raw_material_cost(self, fg_reference_id=None, amount=0): + if not fg_reference_id: + fg_reference_id = self.name + + for row in self.items: + if row.fg_reference_id != fg_reference_id: + continue + + if not row.is_expandable: + row.rate = get_bom_item_rate( + { + "company": self.company, + "item_code": row.item_code, + "bom_no": "", + "qty": row.qty, + "uom": row.uom, + "stock_uom": row.stock_uom, + "conversion_factor": row.conversion_factor, + "sourced_by_supplier": row.sourced_by_supplier, + }, + self, + ) + + row.amount = flt(row.rate) * flt(row.qty) + + else: + row.amount = 0.0 + row.amount = self.get_raw_material_cost(row.name, row.amount) + row.rate = flt(row.amount) / (flt(row.qty) * flt(row.conversion_factor)) + + amount += flt(row.amount) + + return amount + + def set_is_expandable(self): + fg_items = [row.fg_item for row in self.items if row.fg_item != self.item_code] + for row in self.items: + row.is_expandable = 0 + if row.item_code in fg_items: + row.is_expandable = 1 + + def validate_fields(self): + fields = { + "items": "Items", + } + + for field, label in fields.items(): + if not self.get(field): + frappe.throw(_("Please set {0} in BOM Creator {1}").format(label, self.name)) + + def on_submit(self): + self.enqueue_create_boms() + + def enqueue_create_boms(self): + frappe.enqueue( + self.create_boms, + queue="short", + timeout=600, + is_async=True, + ) + + frappe.msgprint( + _("BOMs creation has been enqueued, kindly check the status after some time"), alert=True + ) + + def create_boms(self): + """ + Sample data structure of production_item_wise_rm + production_item_wise_rm = { + (fg_item_code, name): { + "items": [], + "bom_no": "", + "fg_item_data": {} + } + } + """ + + self.db_set("status", "In Progress") + production_item_wise_rm = OrderedDict({}) + production_item_wise_rm.setdefault( + (self.item_code, self.name), frappe._dict({"items": [], "bom_no": "", "fg_item_data": self}) + ) + + for row in self.items: + if row.is_expandable: + if (row.item_code, row.name) not in production_item_wise_rm: + production_item_wise_rm.setdefault( + (row.item_code, row.name), frappe._dict({"items": [], "bom_no": "", "fg_item_data": row}) + ) + + production_item_wise_rm[(row.fg_item, row.fg_reference_id)]["items"].append(row) + + reverse_tree = OrderedDict(reversed(list(production_item_wise_rm.items()))) + + try: + for d in reverse_tree: + fg_item_data = production_item_wise_rm.get(d).fg_item_data + self.create_bom(fg_item_data, production_item_wise_rm) + + frappe.msgprint(_("BOMs created successfully")) + except Exception: + traceback = frappe.get_traceback() + self.db_set( + { + "status": "Failed", + "error_log": traceback, + } + ) + + frappe.msgprint(_("BOMs creation failed")) + + def create_bom(self, row, production_item_wise_rm): + bom = frappe.new_doc("BOM") + bom.update( + { + "item": row.item_code, + "bom_type": "Production", + "quantity": row.qty, + "allow_alternative_item": 1, + "bom_creator": self.name, + "bom_creator_item": row.name if row.name != self.name else "", + "rm_cost_as_per": "Manual", + } + ) + + for field in BOM_FIELDS: + if self.get(field): + bom.set(field, self.get(field)) + + for item in production_item_wise_rm[(row.item_code, row.name)]["items"]: + bom_no = "" + item.do_not_explode = 1 + if (item.item_code, item.name) in production_item_wise_rm: + bom_no = production_item_wise_rm.get((item.item_code, item.name)).bom_no + item.do_not_explode = 0 + + item_args = {} + for field in BOM_ITEM_FIELDS: + item_args[field] = item.get(field) + + item_args.update( + { + "bom_no": bom_no, + "allow_alternative_item": 1, + "allow_scrap_items": 1, + "include_item_in_manufacturing": 1, + } + ) + + bom.append("items", item_args) + + bom.save(ignore_permissions=True) + bom.submit() + + production_item_wise_rm[(row.item_code, row.name)].bom_no = bom.name + + @frappe.whitelist() + def get_default_bom(self, item_code) -> str: + return frappe.get_cached_value("Item", item_code, "default_bom") + + +@frappe.whitelist() +def get_children(doctype=None, parent=None, **kwargs): + if isinstance(kwargs, str): + kwargs = frappe.parse_json(kwargs) + + if isinstance(kwargs, dict): + kwargs = frappe._dict(kwargs) + + fields = [ + "item_code as value", + "is_expandable as expandable", + "parent as parent_id", + "qty", + "idx", + "'BOM Creator Item' as doctype", + "name", + "uom", + "rate", + "amount", + ] + + query_filters = { + "fg_item": parent, + "parent": kwargs.parent_id, + } + + if kwargs.name: + query_filters["name"] = kwargs.name + + return frappe.get_all("BOM Creator Item", fields=fields, filters=query_filters, order_by="idx") + + +@frappe.whitelist() +def add_item(**kwargs): + if isinstance(kwargs, str): + kwargs = frappe.parse_json(kwargs) + + if isinstance(kwargs, dict): + kwargs = frappe._dict(kwargs) + + doc = frappe.get_doc("BOM Creator", kwargs.parent) + item_info = get_item_details(kwargs.item_code) + kwargs.update( + { + "uom": item_info.stock_uom, + "stock_uom": item_info.stock_uom, + "conversion_factor": 1, + } + ) + + doc.append("items", kwargs) + doc.save() + + return doc + + +@frappe.whitelist() +def add_sub_assembly(**kwargs): + if isinstance(kwargs, str): + kwargs = frappe.parse_json(kwargs) + + if isinstance(kwargs, dict): + kwargs = frappe._dict(kwargs) + + doc = frappe.get_doc("BOM Creator", kwargs.parent) + bom_item = frappe.parse_json(kwargs.bom_item) + + name = kwargs.fg_reference_id + parent_row_no = "" + if not kwargs.convert_to_sub_assembly: + item_info = get_item_details(bom_item.item_code) + item_row = doc.append( + "items", + { + "item_code": bom_item.item_code, + "qty": bom_item.qty, + "uom": item_info.stock_uom, + "fg_item": kwargs.fg_item, + "conversion_factor": 1, + "fg_reference_id": name, + "stock_qty": bom_item.qty, + "fg_reference_id": name, + "do_not_explode": 1, + "is_expandable": 1, + "stock_uom": item_info.stock_uom, + }, + ) + + parent_row_no = item_row.idx + name = "" + + for row in bom_item.get("items"): + row = frappe._dict(row) + item_info = get_item_details(row.item_code) + doc.append( + "items", + { + "item_code": row.item_code, + "qty": row.qty, + "fg_item": bom_item.item_code, + "uom": item_info.stock_uom, + "fg_reference_id": name, + "parent_row_no": parent_row_no, + "conversion_factor": 1, + "do_not_explode": 1, + "stock_qty": row.qty, + "stock_uom": item_info.stock_uom, + }, + ) + + doc.save() + + return doc + + +def get_item_details(item_code): + return frappe.get_cached_value( + "Item", item_code, ["item_name", "description", "image", "stock_uom", "default_bom"], as_dict=1 + ) + + +@frappe.whitelist() +def delete_node(**kwargs): + if isinstance(kwargs, str): + kwargs = frappe.parse_json(kwargs) + + if isinstance(kwargs, dict): + kwargs = frappe._dict(kwargs) + + items = get_children(parent=kwargs.fg_item, parent_id=kwargs.parent) + if kwargs.docname: + frappe.delete_doc("BOM Creator Item", kwargs.docname) + + for item in items: + frappe.delete_doc("BOM Creator Item", item.name) + if item.expandable: + delete_node(fg_item=item.value, parent=item.parent_id) + + doc = frappe.get_doc("BOM Creator", kwargs.parent) + doc.set_rate_for_items() + doc.save() + + return doc + + +@frappe.whitelist() +def edit_qty(doctype, docname, qty, parent): + frappe.db.set_value(doctype, docname, "qty", qty) + doc = frappe.get_doc("BOM Creator", parent) + doc.set_rate_for_items() + doc.save() + + return doc diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator_list.js b/erpnext/manufacturing/doctype/bom_creator/bom_creator_list.js new file mode 100644 index 0000000000..423b721e04 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator_list.js @@ -0,0 +1,18 @@ +frappe.listview_settings['BOM Creator'] = { + add_fields: ["status"], + get_indicator: function (doc) { + if (doc.status === "Draft") { + return [__("Draft"), "red", "status,=,Draft"]; + } else if (doc.status === "In Progress") { + return [__("In Progress"), "orange", "status,=,In Progress"]; + } else if (doc.status === "Completed") { + return [__("Completed"), "green", "status,=,Completed"]; + } else if (doc.status === "Cancelled") { + return [__("Cancelled"), "red", "status,=,Cancelled"]; + } else if (doc.status === "Failed") { + return [__("Failed"), "red", "status,=,Failed"]; + } else if (doc.status === "Submitted") { + return [__("Submitted"), "blue", "status,=,Submitted"]; + } + }, +}; diff --git a/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py new file mode 100644 index 0000000000..d239d58131 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py @@ -0,0 +1,240 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import random + +import frappe +from frappe.tests.utils import FrappeTestCase + +from erpnext.manufacturing.doctype.bom_creator.bom_creator import ( + add_item, + add_sub_assembly, + delete_node, + edit_qty, +) +from erpnext.stock.doctype.item.test_item import make_item + + +class TestBOMCreator(FrappeTestCase): + def setUp(self) -> None: + create_items() + + def test_bom_sub_assembly(self): + final_product = "Bicycle" + make_item( + final_product, + { + "item_group": "Raw Material", + "stock_uom": "Nos", + }, + ) + + doc = make_bom_creator( + name="Bicycle BOM with Sub Assembly", + company="_Test Company", + item_code=final_product, + qty=1, + rm_cosy_as_per="Valuation Rate", + currency="INR", + plc_conversion_rate=1, + conversion_rate=1, + ) + + add_sub_assembly( + parent=doc.name, + fg_item=final_product, + fg_reference_id=doc.name, + bom_item={ + "item_code": "Frame Assembly", + "qty": 1, + "items": [ + { + "item_code": "Frame", + "qty": 1, + }, + { + "item_code": "Fork", + "qty": 1, + }, + ], + }, + ) + + doc.reload() + self.assertEqual(doc.items[0].item_code, "Frame Assembly") + + fg_valuation_rate = 0 + for row in doc.items: + if not row.is_expandable: + fg_valuation_rate += row.amount + self.assertEqual(row.fg_item, "Frame Assembly") + self.assertEqual(row.fg_reference_id, doc.items[0].name) + + self.assertEqual(doc.items[0].amount, fg_valuation_rate) + + def test_bom_raw_material(self): + final_product = "Bicycle" + make_item( + final_product, + { + "item_group": "Raw Material", + "stock_uom": "Nos", + }, + ) + + doc = make_bom_creator( + name="Bicycle BOM with Raw Material", + company="_Test Company", + item_code=final_product, + qty=1, + rm_cosy_as_per="Valuation Rate", + currency="INR", + plc_conversion_rate=1, + conversion_rate=1, + ) + + add_item( + parent=doc.name, + fg_item=final_product, + fg_reference_id=doc.name, + item_code="Pedal Assembly", + qty=2, + ) + + doc.reload() + self.assertEqual(doc.items[0].item_code, "Pedal Assembly") + self.assertEqual(doc.items[0].qty, 2) + + fg_valuation_rate = 0 + for row in doc.items: + if not row.is_expandable: + fg_valuation_rate += row.amount + self.assertEqual(row.fg_item, "Bicycle") + self.assertEqual(row.fg_reference_id, doc.name) + + self.assertEqual(doc.raw_material_cost, fg_valuation_rate) + + def test_convert_to_sub_assembly(self): + final_product = "Bicycle" + make_item( + final_product, + { + "item_group": "Raw Material", + "stock_uom": "Nos", + }, + ) + + doc = make_bom_creator( + name="Bicycle BOM", + company="_Test Company", + item_code=final_product, + qty=1, + rm_cosy_as_per="Valuation Rate", + currency="INR", + plc_conversion_rate=1, + conversion_rate=1, + ) + + add_item( + parent=doc.name, + fg_item=final_product, + fg_reference_id=doc.name, + item_code="Pedal Assembly", + qty=2, + ) + + doc.reload() + self.assertEqual(doc.items[0].is_expandable, 0) + + add_sub_assembly( + convert_to_sub_assembly=1, + parent=doc.name, + fg_item=final_product, + fg_reference_id=doc.items[0].name, + bom_item={ + "item_code": "Pedal Assembly", + "qty": 2, + "items": [ + { + "item_code": "Pedal Body", + "qty": 2, + }, + { + "item_code": "Pedal Axle", + "qty": 2, + }, + ], + }, + ) + + doc.reload() + self.assertEqual(doc.items[0].is_expandable, 1) + + fg_valuation_rate = 0 + for row in doc.items: + if not row.is_expandable: + fg_valuation_rate += row.amount + self.assertEqual(row.fg_item, "Pedal Assembly") + self.assertEqual(row.qty, 2.0) + self.assertEqual(row.fg_reference_id, doc.items[0].name) + + self.assertEqual(doc.raw_material_cost, fg_valuation_rate) + + +def create_items(): + raw_materials = [ + "Frame", + "Fork", + "Rim", + "Spokes", + "Hub", + "Tube", + "Tire", + "Pedal Body", + "Pedal Axle", + "Ball Bearings", + "Chain Links", + "Chain Pins", + "Seat", + "Seat Post", + "Seat Clamp", + ] + + for item in raw_materials: + valuation_rate = random.choice([100, 200, 300, 500, 333, 222, 44, 20, 10]) + make_item( + item, + { + "item_group": "Raw Material", + "stock_uom": "Nos", + "valuation_rate": valuation_rate, + }, + ) + + sub_assemblies = [ + "Frame Assembly", + "Wheel Assembly", + "Pedal Assembly", + "Chain Assembly", + "Seat Assembly", + ] + + for item in sub_assemblies: + make_item( + item, + { + "item_group": "Raw Material", + "stock_uom": "Nos", + }, + ) + + +def make_bom_creator(**kwargs): + if isinstance(kwargs, str) or isinstance(kwargs, dict): + kwargs = frappe.parse_json(kwargs) + + doc = frappe.new_doc("BOM Creator") + doc.update(kwargs) + doc.save() + + return doc diff --git a/erpnext/manufacturing/doctype/bom_creator_item/__init__.py b/erpnext/manufacturing/doctype/bom_creator_item/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json new file mode 100644 index 0000000000..fdb5d3ad33 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json @@ -0,0 +1,243 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-07-18 14:35:50.307386", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "item_name", + "item_group", + "column_break_f63f", + "fg_item", + "source_warehouse", + "is_expandable", + "sourced_by_supplier", + "bom_created", + "description_section", + "description", + "quantity_and_rate_section", + "qty", + "rate", + "uom", + "column_break_bgnb", + "stock_qty", + "conversion_factor", + "stock_uom", + "amount_section", + "amount", + "column_break_yuca", + "base_rate", + "base_amount", + "section_break_wtld", + "do_not_explode", + "parent_row_no", + "fg_reference_id", + "column_break_sulm", + "instruction" + ], + "fields": [ + { + "columns": 2, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "reqd": 1 + }, + { + "fetch_from": "item_code.item_name", + "fetch_if_empty": 1, + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name" + }, + { + "fetch_from": "item_code.item_group", + "fieldname": "item_group", + "fieldtype": "Link", + "label": "Item Group", + "options": "Item Group" + }, + { + "fieldname": "column_break_f63f", + "fieldtype": "Column Break" + }, + { + "columns": 2, + "fieldname": "fg_item", + "fieldtype": "Link", + "in_list_view": 1, + "label": "FG Item", + "options": "Item", + "reqd": 1 + }, + { + "fieldname": "source_warehouse", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Source Warehouse", + "options": "Warehouse" + }, + { + "default": "0", + "fieldname": "is_expandable", + "fieldtype": "Check", + "label": "Is Expandable", + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "description_section", + "fieldtype": "Section Break", + "label": "Description" + }, + { + "fetch_from": "item_code.description", + "fetch_if_empty": 1, + "fieldname": "description", + "fieldtype": "Small Text" + }, + { + "fieldname": "quantity_and_rate_section", + "fieldtype": "Section Break", + "label": "Quantity and Rate" + }, + { + "columns": 1, + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Qty" + }, + { + "columns": 2, + "fieldname": "rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Rate" + }, + { + "columns": 1, + "fieldname": "uom", + "fieldtype": "Link", + "in_list_view": 1, + "label": "UOM", + "options": "UOM" + }, + { + "fieldname": "column_break_bgnb", + "fieldtype": "Column Break" + }, + { + "fieldname": "stock_qty", + "fieldtype": "Float", + "label": "Stock Qty", + "read_only": 1 + }, + { + "fieldname": "conversion_factor", + "fieldtype": "Float", + "label": "Conversion Factor" + }, + { + "fetch_from": "item_code.stock_uom", + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "no_copy": 1, + "options": "UOM", + "read_only": 1 + }, + { + "fieldname": "amount_section", + "fieldtype": "Section Break", + "label": "Amount" + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount", + "read_only": 1 + }, + { + "fieldname": "column_break_yuca", + "fieldtype": "Column Break" + }, + { + "default": "1", + "fieldname": "do_not_explode", + "fieldtype": "Check", + "hidden": 1, + "label": "Do Not Explode" + }, + { + "fieldname": "instruction", + "fieldtype": "Small Text", + "label": "Instruction" + }, + { + "fieldname": "base_amount", + "fieldtype": "Currency", + "hidden": 1, + "label": "Base Amount" + }, + { + "fieldname": "base_rate", + "fieldtype": "Currency", + "hidden": 1, + "label": "Base Rate" + }, + { + "default": "0", + "fieldname": "sourced_by_supplier", + "fieldtype": "Check", + "label": "Sourced by Supplier" + }, + { + "fieldname": "section_break_wtld", + "fieldtype": "Section Break" + }, + { + "fieldname": "fg_reference_id", + "fieldtype": "Data", + "label": "FG Reference", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_sulm", + "fieldtype": "Column Break" + }, + { + "fieldname": "parent_row_no", + "fieldtype": "Data", + "label": "Parent Row No", + "no_copy": 1, + "print_hide": 1 + }, + { + "default": "0", + "fieldname": "bom_created", + "fieldtype": "Check", + "hidden": 1, + "label": "BOM Created", + "no_copy": 1, + "print_hide": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-08-07 11:52:30.492233", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "BOM Creator Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py new file mode 100644 index 0000000000..350c9180b9 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class BOMCreatorItem(Document): + pass diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json index 518ae14659..8e0785074f 100644 --- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json +++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"PIQJYZOMnD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Manufacturing\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", + "content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"YHCQG3wAGv\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Creator\",\"col\":3}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"PIQJYZOMnD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Manufacturing\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", "creation": "2020-03-02 17:11:37.032604", "custom_blocks": [], "docstatus": 0, @@ -316,7 +316,7 @@ "type": "Link" } ], - "modified": "2023-07-04 14:40:47.281125", + "modified": "2023-08-08 22:28:39.633891", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing", @@ -336,6 +336,13 @@ "type": "URL", "url": "https://frappe.school/courses/manufacturing?utm_source=in_app" }, + { + "color": "Grey", + "doc_view": "List", + "label": "BOM Creator", + "link_to": "BOM Creator", + "type": "DocType" + }, { "color": "Grey", "doc_view": "List", diff --git a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js new file mode 100644 index 0000000000..b3b2e9f9b8 --- /dev/null +++ b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js @@ -0,0 +1,416 @@ +class BOMConfigurator { + constructor({ wrapper, page, frm, bom_configurator }) { + this.$wrapper = $(wrapper); + this.page = page; + this.bom_configurator = bom_configurator; + this.frm = frm; + + this.make(); + this.prepare_layout(); + this.bind_events(); + } + + add_boms() { + this.frm.call({ + method: "add_boms", + freeze: true, + doc: this.frm.doc, + }); + } + + make() { + let options = { + ...this.tree_options(), + ...this.tree_methods(), + }; + + frappe.views.trees["BOM Configurator"] = new frappe.views.TreeView(options); + this.tree_view = frappe.views.trees["BOM Configurator"]; + } + + bind_events() { + frappe.views.trees["BOM Configurator"].events = { + frm: this.frm, + add_item: this.add_item, + add_sub_assembly: this.add_sub_assembly, + get_sub_assembly_modal_fields: this.get_sub_assembly_modal_fields, + convert_to_sub_assembly: this.convert_to_sub_assembly, + delete_node: this.delete_node, + edit_qty: this.edit_qty, + load_tree: this.load_tree, + set_default_qty: this.set_default_qty, + } + } + + tree_options() { + return { + parent: this.$wrapper.get(0), + body: this.$wrapper.get(0), + doctype: 'BOM Configurator', + page: this.page, + expandable: true, + title: __("Configure Product Assembly"), + breadcrumb: "Manufacturing", + get_tree_nodes: "erpnext.manufacturing.doctype.bom_creator.bom_creator.get_children", + root_label: this.frm.doc.item_code, + disable_add_node: true, + get_tree_root: false, + show_expand_all: false, + extend_toolbar: false, + do_not_make_page: true, + do_not_setup_menu: true, + } + } + + tree_methods() { + let frm_obj = this; + let view = frappe.views.trees["BOM Configurator"]; + + return { + onload: function(me) { + me.args["parent_id"] = frm_obj.frm.doc.name; + me.args["parent"] = frm_obj.frm.doc.item_code; + me.parent = frm_obj.$wrapper.get(0); + me.body = frm_obj.$wrapper.get(0); + me.make_tree(); + }, + onrender(node) { + const qty = node.data.qty || frm_obj.frm.doc.qty; + const uom = node.data.uom || frm_obj.frm.doc.uom; + const docname = node.data.name || frm_obj.frm.doc.name; + let amount = node.data.amount; + if (node.data.value === frm_obj.frm.doc.item_code) { + amount = frm_obj.frm.doc.raw_material_cost; + } + + amount = frappe.format(amount, { fieldtype: "Currency", currency: frm_obj.frm.doc.currency }); + + $(` +
+
${qty} ${uom}
+
+ ${amount} +
+
+ + `).insertBefore(node.$ul); + }, + toolbar: this.frm?.doc.docstatus === 0 ? [ + { + label:__(frappe.utils.icon('edit', 'sm') + " Qty"), + click: function(node) { + let view = frappe.views.trees["BOM Configurator"]; + view.events.edit_qty(node, view); + }, + btnClass: "hidden-xs" + }, + { + label:__(frappe.utils.icon('add', 'sm') + " Raw Material"), + click: function(node) { + let view = frappe.views.trees["BOM Configurator"]; + view.events.add_item(node, view); + }, + condition: function(node) { + return node.expandable; + }, + btnClass: "hidden-xs" + }, + { + label:__(frappe.utils.icon('add', 'sm') + " Sub Assembly"), + click: function(node) { + let view = frappe.views.trees["BOM Configurator"]; + view.events.add_sub_assembly(node, view); + }, + condition: function(node) { + return node.expandable; + }, + btnClass: "hidden-xs" + }, + { + label:__("Expand All"), + click: function(node) { + let view = frappe.views.trees["BOM Configurator"]; + + if (!node.expanded) { + view.tree.load_children(node, true); + $(node.parent[0]).find(".tree-children").show(); + node.$toolbar.find(".expand-all-btn").html("Collapse All"); + } else { + node.$tree_link.trigger("click"); + node.$toolbar.find(".expand-all-btn").html("Expand All"); + } + }, + condition: function(node) { + return node.expandable && node.is_root; + }, + btnClass: "hidden-xs expand-all-btn" + }, + { + label:__(frappe.utils.icon('move', 'sm') + " Sub Assembly"), + click: function(node) { + let view = frappe.views.trees["BOM Configurator"]; + view.events.convert_to_sub_assembly(node, view); + }, + condition: function(node) { + return !node.expandable; + }, + btnClass: "hidden-xs" + }, + { + label:__(frappe.utils.icon('delete', 'sm') + __(" Item")), + click: function(node) { + let view = frappe.views.trees["BOM Configurator"]; + view.events.delete_node(node, view); + }, + condition: function(node) { + return !node.is_root; + }, + btnClass: "hidden-xs" + }, + ] : [{ + label:__("Expand All"), + click: function(node) { + let view = frappe.views.trees["BOM Configurator"]; + + if (!node.expanded) { + view.tree.load_children(node, true); + $(node.parent[0]).find(".tree-children").show(); + node.$toolbar.find(".expand-all-btn").html("Collapse All"); + } else { + node.$tree_link.trigger("click"); + node.$toolbar.find(".expand-all-btn").html("Expand All"); + } + }, + condition: function(node) { + return node.expandable && node.is_root; + }, + btnClass: "hidden-xs expand-all-btn" + }], + } + } + + add_item(node, view) { + frappe.prompt([ + { label: __("Item"), fieldname: "item_code", fieldtype: "Link", options: "Item", reqd: 1 }, + { label: __("Qty"), fieldname: "qty", default: 1.0, fieldtype: "Float", reqd: 1 }, + ], + (data) => { + if (!node.data.parent_id) { + node.data.parent_id = this.frm.doc.name; + } + + frappe.call({ + method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_item", + args: { + parent: node.data.parent_id, + fg_item: node.data.value, + item_code: data.item_code, + fg_reference_id: node.data.name || this.frm.doc.name, + qty: data.qty, + }, + callback: (r) => { + view.events.load_tree(r, node); + } + }); + }, + __("Add Item"), + __("Add")); + } + + add_sub_assembly(node, view) { + let dialog = new frappe.ui.Dialog({ + fields: view.events.get_sub_assembly_modal_fields(), + title: __("Add Sub Assembly"), + }); + + dialog.show(); + view.events.set_default_qty(dialog); + + dialog.set_primary_action(__("Add"), () => { + let bom_item = dialog.get_values(); + + if (!node.data?.parent_id) { + node.data.parent_id = this.frm.doc.name; + } + + frappe.call({ + method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_sub_assembly", + args: { + parent: node.data.parent_id, + fg_item: node.data.value, + fg_reference_id: node.data.name || this.frm.doc.name, + bom_item: bom_item, + }, + callback: (r) => { + view.events.load_tree(r, node); + } + }); + + dialog.hide(); + }); + + } + + get_sub_assembly_modal_fields(read_only=false) { + return [ + { label: __("Sub Assembly Item"), fieldname: "item_code", fieldtype: "Link", options: "Item", reqd: 1, read_only: read_only }, + { fieldtype: "Column Break" }, + { label: __("Qty"), fieldname: "qty", default: 1.0, fieldtype: "Float", reqd: 1, read_only: read_only }, + { fieldtype: "Section Break" }, + { label: __("Raw Materials"), fieldname: "items", fieldtype: "Table", reqd: 1, + fields: [ + { label: __("Item"), fieldname: "item_code", fieldtype: "Link", options: "Item", reqd: 1, in_list_view: 1 }, + { label: __("Qty"), fieldname: "qty", default: 1.0, fieldtype: "Float", reqd: 1, in_list_view: 1 }, + ] + }, + ] + } + + convert_to_sub_assembly(node, view) { + let dialog = new frappe.ui.Dialog({ + fields: view.events.get_sub_assembly_modal_fields(true), + title: __("Add Sub Assembly"), + }); + + dialog.set_values({ + item_code: node.data.value, + qty: node.data.qty, + }); + + dialog.show(); + view.events.set_default_qty(dialog); + + dialog.set_primary_action(__("Add"), () => { + let bom_item = dialog.get_values(); + + frappe.call({ + method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_sub_assembly", + args: { + parent: node.data.parent_id, + fg_item: node.data.value, + bom_item: bom_item, + fg_reference_id: node.data.name || this.frm.doc.name, + convert_to_sub_assembly: true, + }, + callback: (r) => { + node.expandable = true; + view.events.load_tree(r, node); + } + }); + + dialog.hide(); + }); + } + + set_default_qty(dialog) { + dialog.fields_dict.items.grid.fields_map.item_code.onchange = function (event) { + if (event) { + let name = $(event.currentTarget).closest('.grid-row').attr("data-name") + let item_row = dialog.fields_dict.items.grid.grid_rows_by_docname[name].doc; + item_row.qty = 1; + dialog.fields_dict.items.grid.refresh() + } + } + } + + delete_node(node, view) { + frappe.confirm(__("Are you sure you want to delete this Item?"), () => { + frappe.call({ + method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.delete_node", + args: { + parent: node.data.parent_id, + fg_item: node.data.value, + doctype: node.data.doctype, + docname: node.data.name, + }, + callback: (r) => { + view.events.load_tree(r, node.parent_node); + } + }); + }); + } + + edit_qty(node, view) { + let qty = node.data.qty || this.frm.doc.qty; + frappe.prompt([ + { label: __("Qty"), fieldname: "qty", default: qty, fieldtype: "Float", reqd: 1 }, + ], + (data) => { + let doctype = node.data.doctype || this.frm.doc.doctype; + let docname = node.data.name || this.frm.doc.name; + + frappe.call({ + method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.edit_qty", + args: { + doctype: doctype, + docname: docname, + qty: data.qty, + parent: node.data.parent_id, + }, + callback: (r) => { + node.data.qty = data.qty; + let uom = node.data.uom || this.frm.doc.uom; + $(node.parent.get(0)).find(`[data-bom-qty-docname='${docname}']`).html(data.qty + " " + uom); + view.events.load_tree(r, node); + } + }); + }, + __("Edit Qty"), + __("Update")); + } + + prepare_layout() { + let main_div = $(this.page)[0]; + + main_div.style.marginBottom = "15px"; + $(main_div).find(".tree-children")[0].style.minHeight = "370px"; + $(main_div).find(".tree-children")[0].style.maxHeight = "370px"; + $(main_div).find(".tree-children")[0].style.overflowY = "auto"; + } + + load_tree(response, node) { + let item_row = ""; + let parent_dom = "" + let total_amount = response.message.raw_material_cost; + + frappe.views.trees["BOM Configurator"].tree.load_children(node); + + while (true) { + item_row = response.message.items.filter(item => item.name === node.data.name); + + if (item_row?.length) { + node.data.amount = item_row[0].amount; + total_amount = node.data.amount + } else { + total_amount = response.message.raw_material_cost; + } + + parent_dom = $(node.parent.get(0)); + total_amount = frappe.format( + total_amount, { + fieldtype: "Currency", + currency: this.frm.doc.currency + } + ); + + $($(parent_dom).find(".fg-item-amt")[0]).html(total_amount); + + if (node.is_root) { + break; + } + + node = node.parent_node; + } + + } +} + +frappe.ui.BOMConfigurator = BOMConfigurator; \ No newline at end of file diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index c11d123982..a3c10c68a7 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -114,6 +114,10 @@ $.extend(erpnext.utils, { }, view_serial_batch_nos: function(frm) { + if (!frm.doc?.items) { + return; + } + let bundle_ids = frm.doc.items.filter(d => d.serial_and_batch_bundle); if (bundle_ids?.length) { From 6349b67df497623ce3f56da3d9f3c94295c3dd3b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Aug 2023 17:54:14 +0530 Subject: [PATCH 53/60] fix(demo): Default accounts for demo company --- .../verified/id_chart_of_accounts.json | 3 +-- erpnext/public/js/setup_wizard.js | 3 ++- erpnext/setup/doctype/company/company.py | 16 ++++++++++------ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json index d1a0defba9..12f517d64b 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json @@ -69,8 +69,7 @@ "Persediaan Barang": { "Persediaan Barang": { "account_number": "1141.000", - "account_type": "Stock", - "is_group": 1 + "account_type": "Stock" }, "Uang Muka Pembelian": { "Uang Muka Pembelian": { diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js index ba200ef168..3cbec3e8ef 100644 --- a/erpnext/public/js/setup_wizard.js +++ b/erpnext/public/js/setup_wizard.js @@ -45,7 +45,8 @@ erpnext.setup.slides_settings = [ fieldname: 'setup_demo', label: __('Generate Demo Data for Exploration'), fieldtype: 'Check', - description: 'If checked, we will create demo data for you to explore the system. This demo data can be erased later.'}, + description: __('If checked, we will create demo data for you to explore the system. This demo data can be erased later.') + }, ], onload: function (slide) { diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index fcdf245659..2db27528cc 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -403,14 +403,18 @@ class Company(NestedSet): self._set_default_account(default_account, default_accounts.get(default_account)) if not self.default_income_account: - income_account = frappe.db.get_value( - "Account", {"account_name": _("Sales"), "company": self.name, "is_group": 0} + income_account = frappe.db.get_all( + "Account", + filters={"company": self.name, "is_group": 0}, + or_filters={ + "account_name": ("in", [_("Sales"), _("Sales Account")]), + "account_type": "Income Account", + }, + pluck="name", ) - if not income_account: - income_account = frappe.db.get_value( - "Account", {"account_name": _("Sales Account"), "company": self.name} - ) + if income_account: + income_account = income_account[0] self.db_set("default_income_account", income_account) From 5f75aea6fab48675f4606d5d9b36425d2d7cbbc4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Aug 2023 17:58:51 +0530 Subject: [PATCH 54/60] fix(demo): Default accounts for demo company --- .../verified/ae_uae_chart_template_standard.json | 16 ++++++++++++---- .../verified/id_chart_of_accounts.json | 3 ++- erpnext/accounts/general_ledger.py | 4 ++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/ae_uae_chart_template_standard.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/ae_uae_chart_template_standard.json index a8afb55df6..3a3b6e399e 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/ae_uae_chart_template_standard.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/ae_uae_chart_template_standard.json @@ -437,12 +437,20 @@ }, "Sales": { "Sales from Other Regions": { - "Sales from Other Region": {} + "Sales from Other Region": { + "account_type": "Income Account" + } }, "Sales of same region": { - "Management Consultancy Fees 1": {}, - "Sales Account": {}, - "Sales of I/C": {} + "Management Consultancy Fees 1": { + "account_type": "Income Account" + }, + "Sales Account": { + "account_type": "Income Account" + }, + "Sales of I/C": { + "account_type": "Income Account" + } } }, "root_type": "Income" diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json index 12f517d64b..fb974765db 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json @@ -669,7 +669,8 @@ }, "Penjualan Barang Dagangan": { "Penjualan": { - "account_number": "4110.000" + "account_number": "4110.000", + "account_type": "Income Account" }, "Potongan Penjualan": { "account_number": "4130.000" diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 3803836ef7..d4967785ba 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -539,6 +539,10 @@ def get_round_off_account_and_cost_center( "Company", company, ["round_off_account", "round_off_cost_center"] ) or [None, None] + # Use expense account as fallback + if not round_off_account: + round_off_account = frappe.get_cached_value("Company", company, "default_expense_account") + meta = frappe.get_meta(voucher_type) # Give first preference to parent cost center for round off GLE From 299e32befd50d32129ca58cba386660cd0179dfb Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Aug 2023 18:02:06 +0530 Subject: [PATCH 55/60] chore: Linting Issues --- erpnext/accounts/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 9d6d0f91fb..1aefeaacf7 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -908,9 +908,9 @@ def get_outstanding_invoices( min_outstanding=None, max_outstanding=None, accounting_dimensions=None, - vouchers=None, # list of dicts [{'voucher_type': '', 'voucher_no': ''}] for filtering - limit=None, # passed by reconciliation tool - voucher_no=None, # filter passed by reconciliation tool + vouchers=None, # list of dicts [{'voucher_type': '', 'voucher_no': ''}] for filtering + limit=None, # passed by reconciliation tool + voucher_no=None, # filter passed by reconciliation tool ): ple = qb.DocType("Payment Ledger Entry") From 0120588f5fa6cc3cb7791dac8cb86c788dc21f19 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Aug 2023 18:32:16 +0530 Subject: [PATCH 56/60] chore: Handle edge cases --- erpnext/setup/doctype/company/company.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 2db27528cc..b05696ad96 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -415,6 +415,8 @@ class Company(NestedSet): if income_account: income_account = income_account[0] + else: + income_account = None self.db_set("default_income_account", income_account) From 388a42ec7ee0e6a2a36b81c6f3c84b07deb8b12c Mon Sep 17 00:00:00 2001 From: ViralKansodiya <141210323+viralkansodiya@users.noreply.github.com> Date: Fri, 25 Aug 2023 11:44:05 +0530 Subject: [PATCH 57/60] =?UTF-8?q?fix:=20Asset=20Category=20filter=20is=20n?= =?UTF-8?q?ot=20working=20in=20asset=20depreciation=20and=20b=E2=80=A6=20(?= =?UTF-8?q?#36806)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: Asset Category filter is not working in asset depreciation and balances Co-authored-by: ubuntu --- .../asset_depreciations_and_balances.py | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py index d67eee3552..bdc8d8504f 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py @@ -58,6 +58,9 @@ def get_data(filters): def get_asset_categories(filters): + condition = "" + if filters.get("asset_category"): + condition += " and asset_category = %(asset_category)s" return frappe.db.sql( """ SELECT asset_category, @@ -98,15 +101,25 @@ def get_asset_categories(filters): 0 end), 0) as cost_of_scrapped_asset from `tabAsset` - where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s + where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {} group by asset_category - """, - {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, + """.format( + condition + ), + { + "to_date": filters.to_date, + "from_date": filters.from_date, + "company": filters.company, + "asset_category": filters.get("asset_category"), + }, as_dict=1, ) def get_assets(filters): + condition = "" + if filters.get("asset_category"): + condition = " and a.asset_category = '{}'".format(filters.get("asset_category")) return frappe.db.sql( """ SELECT results.asset_category, @@ -138,7 +151,7 @@ def get_assets(filters): aca.parent = a.asset_category and aca.company_name = %(company)s join `tabCompany` company on company.name = %(company)s - where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) + where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0} group by a.asset_category union SELECT a.asset_category, @@ -154,10 +167,12 @@ def get_assets(filters): end), 0) as depreciation_eliminated_during_the_period, 0 as depreciation_amount_during_the_period from `tabAsset` a - where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s + where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0} group by a.asset_category) as results group by results.asset_category - """, + """.format( + condition + ), {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, as_dict=1, ) From bb7bed4c1a10aa49bbb1d14654454866918cf35c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 24 Aug 2023 20:46:33 +0530 Subject: [PATCH 58/60] refactor(test): make use of mixin in ar/ap report tests --- .../test_accounts_receivable.py | 211 ++++++++---------- erpnext/accounts/test/accounts_mixin.py | 25 ++- 2 files changed, 121 insertions(+), 115 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 6f1889b34e..0099e79e5d 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -8,20 +8,17 @@ from erpnext import get_default_cost_center from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order -class TestAccountsReceivable(FrappeTestCase): +class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): def setUp(self): - frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'") - frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'") - frappe.db.sql("delete from `tabPayment Entry` where company='_Test Company 2'") - frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'") - frappe.db.sql("delete from `tabPayment Ledger Entry` where company='_Test Company 2'") - frappe.db.sql("delete from `tabJournal Entry` where company='_Test Company 2'") - frappe.db.sql("delete from `tabExchange Rate Revaluation` where company='_Test Company 2'") - - self.create_usd_account() + self.create_company() + self.create_customer() + self.create_item() + self.create_usd_receivable_account() + self.clear_old_entries() def tearDown(self): frappe.db.rollback() @@ -49,9 +46,61 @@ class TestAccountsReceivable(FrappeTestCase): debtors_usd.account_type = debtors.account_type self.debtors_usd = debtors_usd.save().name + def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False): + frappe.set_user("Administrator") + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + price_list_rate=100, + do_not_save=1, + ) + if not no_payment_schedule: + si.append( + "payment_schedule", + dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30), + ) + si.append( + "payment_schedule", + dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50), + ) + si.append( + "payment_schedule", + dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20), + ) + si = si.save() + if not do_not_submit: + si = si.submit() + return si + + def create_payment_entry(self, docname): + pe = get_payment_entry("Sales Invoice", docname, bank_account=self.cash, party_amount=40) + pe.paid_from = self.debit_to + pe.insert() + pe.submit() + + def create_credit_note(self, docname): + credit_note = create_sales_invoice( + company=self.company, + customer=self.customer, + item=self.item, + qty=-1, + debit_to=self.debit_to, + cost_center=self.cost_center, + is_return=1, + return_against=docname, + ) + + return credit_note + def test_accounts_receivable(self): filters = { - "company": "_Test Company 2", + "company": self.company, "based_on_payment_terms": 1, "report_date": today(), "range1": 30, @@ -61,7 +110,9 @@ class TestAccountsReceivable(FrappeTestCase): } # check invoice grand total and invoiced column's value for 3 payment terms - name = make_sales_invoice().name + si = self.create_sales_invoice() + name = si.name + report = execute(filters) expected_data = [[100, 30], [100, 50], [100, 20]] @@ -71,7 +122,7 @@ class TestAccountsReceivable(FrappeTestCase): self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced]) # check invoice grand total, invoiced, paid and outstanding column's value after payment - make_payment(name) + self.create_payment_entry(si.name) report = execute(filters) expected_data_after_payment = [[100, 50, 10, 40], [100, 20, 0, 20]] @@ -84,10 +135,10 @@ class TestAccountsReceivable(FrappeTestCase): ) # check invoice grand total, invoiced, paid and outstanding column's value after credit note - make_credit_note(name) + self.create_credit_note(si.name) report = execute(filters) - expected_data_after_credit_note = [100, 0, 0, 40, -40, "Debtors - _TC2"] + expected_data_after_credit_note = [100, 0, 0, 40, -40, self.debit_to] row = report[1][0] self.assertEqual( @@ -108,21 +159,20 @@ class TestAccountsReceivable(FrappeTestCase): """ so = make_sales_order( - company="_Test Company 2", - customer="_Test Customer 2", - warehouse="Finished Goods - _TC2", - currency="EUR", - debit_to="Debtors - _TC2", - income_account="Sales - _TC2", - expense_account="Cost of Goods Sold - _TC2", - cost_center="Main - _TC2", + company=self.company, + customer=self.customer, + warehouse=self.warehouse, + debit_to=self.debit_to, + income_account=self.income_account, + expense_account=self.expense_account, + cost_center=self.cost_center, ) pe = get_payment_entry(so.doctype, so.name) pe = pe.save().submit() filters = { - "company": "_Test Company 2", + "company": self.company, "based_on_payment_terms": 0, "report_date": today(), "range1": 30, @@ -147,34 +197,32 @@ class TestAccountsReceivable(FrappeTestCase): ) @change_settings( - "Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1} + "Accounts Settings", + {"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0}, ) def test_exchange_revaluation_for_party(self): """ - Exchange Revaluation for party on Receivable/Payable shoule be included + Exchange Revaluation for party on Receivable/Payable should be included """ - company = "_Test Company 2" - customer = "_Test Customer 2" - # Using Exchange Gain/Loss account for unrealized as well. - company_doc = frappe.get_doc("Company", company) + company_doc = frappe.get_doc("Company", self.company) company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account company_doc.save() - si = make_sales_invoice(no_payment_schedule=True, do_not_submit=True) + si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True) si.currency = "USD" - si.conversion_rate = 0.90 + si.conversion_rate = 80 si.debit_to = self.debtors_usd si = si.save().submit() # Exchange Revaluation err = frappe.new_doc("Exchange Rate Revaluation") - err.company = company + err.company = self.company err.posting_date = today() accounts = err.get_accounts_data() err.extend("accounts", accounts) - err.accounts[0].new_exchange_rate = 0.95 + err.accounts[0].new_exchange_rate = 85 row = err.accounts[0] row.new_balance_in_base_currency = flt( row.new_exchange_rate * flt(row.balance_in_account_currency) @@ -189,7 +237,7 @@ class TestAccountsReceivable(FrappeTestCase): je = je.submit() filters = { - "company": company, + "company": self.company, "report_date": today(), "range1": 30, "range2": 60, @@ -198,7 +246,7 @@ class TestAccountsReceivable(FrappeTestCase): } report = execute(filters) - expected_data_for_err = [0, -5, 0, 5] + expected_data_for_err = [0, -500, 0, 500] row = [x for x in report[1] if x.voucher_type == je.doctype and x.voucher_no == je.name][0] self.assertEqual( expected_data_for_err, @@ -214,46 +262,43 @@ class TestAccountsReceivable(FrappeTestCase): """ Payment against credit/debit note should be considered against the parent invoice """ - company = "_Test Company 2" - customer = "_Test Customer 2" - si1 = make_sales_invoice() + si1 = self.create_sales_invoice() - pe = get_payment_entry("Sales Invoice", si1.name, bank_account="Cash - _TC2") - pe.paid_from = "Debtors - _TC2" + pe = get_payment_entry(si1.doctype, si1.name, bank_account=self.cash) + pe.paid_from = self.debit_to pe.insert() pe.submit() - cr_note = make_credit_note(si1.name) + cr_note = self.create_credit_note(si1.name) - si2 = make_sales_invoice() + si2 = self.create_sales_invoice() # manually link cr_note with si2 using journal entry je = frappe.new_doc("Journal Entry") - je.company = company + je.company = self.company je.voucher_type = "Credit Note" je.posting_date = today() - debit_account = "Debtors - _TC2" debit_entry = { - "account": debit_account, + "account": self.debit_to, "party_type": "Customer", - "party": customer, + "party": self.customer, "debit": 100, "debit_in_account_currency": 100, "reference_type": cr_note.doctype, "reference_name": cr_note.name, - "cost_center": "Main - _TC2", + "cost_center": self.cost_center, } credit_entry = { - "account": debit_account, + "account": self.debit_to, "party_type": "Customer", - "party": customer, + "party": self.customer, "credit": 100, "credit_in_account_currency": 100, "reference_type": si2.doctype, "reference_name": si2.name, - "cost_center": "Main - _TC2", + "cost_center": self.cost_center, } je.append("accounts", debit_entry) @@ -261,7 +306,7 @@ class TestAccountsReceivable(FrappeTestCase): je = je.save().submit() filters = { - "company": company, + "company": self.company, "report_date": today(), "range1": 30, "range2": 60, @@ -270,65 +315,3 @@ class TestAccountsReceivable(FrappeTestCase): } report = execute(filters) self.assertEqual(report[1], []) - - -def make_sales_invoice(no_payment_schedule=False, do_not_submit=False): - frappe.set_user("Administrator") - - si = create_sales_invoice( - company="_Test Company 2", - customer="_Test Customer 2", - currency="EUR", - warehouse="Finished Goods - _TC2", - debit_to="Debtors - _TC2", - income_account="Sales - _TC2", - expense_account="Cost of Goods Sold - _TC2", - cost_center="Main - _TC2", - do_not_save=1, - ) - - if not no_payment_schedule: - si.append( - "payment_schedule", - dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30), - ) - si.append( - "payment_schedule", - dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50), - ) - si.append( - "payment_schedule", - dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20), - ) - - si = si.save() - - if not do_not_submit: - si = si.submit() - - return si - - -def make_payment(docname): - pe = get_payment_entry("Sales Invoice", docname, bank_account="Cash - _TC2", party_amount=40) - pe.paid_from = "Debtors - _TC2" - pe.insert() - pe.submit() - - -def make_credit_note(docname): - credit_note = create_sales_invoice( - company="_Test Company 2", - customer="_Test Customer 2", - currency="EUR", - qty=-1, - warehouse="Finished Goods - _TC2", - debit_to="Debtors - _TC2", - income_account="Sales - _TC2", - expense_account="Cost of Goods Sold - _TC2", - cost_center="Main - _TC2", - is_return=1, - return_against=docname, - ) - - return credit_note diff --git a/erpnext/accounts/test/accounts_mixin.py b/erpnext/accounts/test/accounts_mixin.py index debfffdcbb..bf01362c97 100644 --- a/erpnext/accounts/test/accounts_mixin.py +++ b/erpnext/accounts/test/accounts_mixin.py @@ -60,7 +60,6 @@ class AccountsTestMixin: self.income_account = "Sales - " + abbr self.expense_account = "Cost of Goods Sold - " + abbr self.debit_to = "Debtors - " + abbr - self.debit_usd = "Debtors USD - " + abbr self.cash = "Cash - " + abbr self.creditors = "Creditors - " + abbr self.retained_earnings = "Retained Earnings - " + abbr @@ -105,6 +104,28 @@ class AccountsTestMixin: new_acc.save() setattr(self, acc.attribute_name, new_acc.name) + def create_usd_receivable_account(self): + account_name = "Debtors USD" + if not frappe.db.get_value( + "Account", filters={"account_name": account_name, "company": self.company} + ): + acc = frappe.new_doc("Account") + acc.account_name = account_name + acc.parent_account = "Accounts Receivable - " + self.company_abbr + acc.company = self.company + acc.account_currency = "USD" + acc.account_type = "Receivable" + acc.insert() + else: + name = frappe.db.get_value( + "Account", + filters={"account_name": account_name, "company": self.company}, + fieldname="name", + pluck=True, + ) + acc = frappe.get_doc("Account", name) + self.debtors_usd = acc.name + def clear_old_entries(self): doctype_list = [ "GL Entry", @@ -113,6 +134,8 @@ class AccountsTestMixin: "Purchase Invoice", "Payment Entry", "Journal Entry", + "Sales Order", + "Exchange Rate Revaluation", ] for doctype in doctype_list: qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run() From ce81ffd84438a9fe9df251507adea50e1f55f089 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 25 Aug 2023 15:41:18 +0530 Subject: [PATCH 59/60] test: increase coverage in ar/ap report --- .../test_accounts_receivable.py | 257 +++++++++++++++++- 1 file changed, 255 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 0099e79e5d..0c7d931d2d 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -107,6 +107,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): "range2": 60, "range3": 90, "range4": 120, + "show_remarks": True, } # check invoice grand total and invoiced column's value for 3 payment terms @@ -115,11 +116,11 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): report = execute(filters) - expected_data = [[100, 30], [100, 50], [100, 20]] + expected_data = [[100, 30, "No Remarks"], [100, 50, "No Remarks"], [100, 20, "No Remarks"]] for i in range(3): row = report[1][i - 1] - self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced]) + self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks]) # check invoice grand total, invoiced, paid and outstanding column's value after payment self.create_payment_entry(si.name) @@ -315,3 +316,255 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): } report = execute(filters) self.assertEqual(report[1], []) + + def test_group_by_party(self): + si1 = self.create_sales_invoice(do_not_submit=True) + si1.posting_date = add_days(today(), -1) + si1.save().submit() + si2 = self.create_sales_invoice(do_not_submit=True) + si2.items[0].rate = 85 + si2.save().submit() + + filters = { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "group_by_party": True, + } + report = execute(filters)[1] + self.assertEqual(len(report), 5) + + # assert voucher rows + expected_voucher_rows = [ + [100.0, 100.0, 100.0, 100.0], + [85.0, 85.0, 85.0, 85.0], + ] + voucher_rows = [] + for x in report[0:2]: + voucher_rows.append( + [x.invoiced, x.outstanding, x.invoiced_in_account_currency, x.outstanding_in_account_currency] + ) + self.assertEqual(expected_voucher_rows, voucher_rows) + + # assert total rows + expected_total_rows = [ + [self.customer, 185.0, 185.0], # party total + {}, # empty row for padding + ["Total", 185.0, 185.0], # grand total + ] + party_total_row = report[2] + self.assertEqual( + expected_total_rows[0], + [ + party_total_row.get("party"), + party_total_row.get("invoiced"), + party_total_row.get("outstanding"), + ], + ) + empty_row = report[3] + self.assertEqual(expected_total_rows[1], empty_row) + grand_total_row = report[4] + self.assertEqual( + expected_total_rows[2], + [ + grand_total_row.get("party"), + grand_total_row.get("invoiced"), + grand_total_row.get("outstanding"), + ], + ) + + def test_future_payments(self): + si = self.create_sales_invoice() + pe = get_payment_entry(si.doctype, si.name) + pe.posting_date = add_days(today(), 1) + pe.paid_amount = 90.0 + pe.references[0].allocated_amount = 90.0 + pe.save().submit() + filters = { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "show_future_payments": True, + } + report = execute(filters)[1] + self.assertEqual(len(report), 1) + + expected_data = [100.0, 100.0, 10.0, 90.0] + + row = report[0] + self.assertEqual( + expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount] + ) + + pe.cancel() + # full payment in future date + pe = get_payment_entry(si.doctype, si.name) + pe.posting_date = add_days(today(), 1) + pe.save().submit() + report = execute(filters)[1] + self.assertEqual(len(report), 1) + expected_data = [100.0, 100.0, 0.0, 100.0] + row = report[0] + self.assertEqual( + expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount] + ) + + pe.cancel() + # over payment in future date + pe = get_payment_entry(si.doctype, si.name) + pe.posting_date = add_days(today(), 1) + pe.paid_amount = 110 + pe.save().submit() + report = execute(filters)[1] + self.assertEqual(len(report), 2) + expected_data = [[100.0, 0.0, 100.0, 0.0, 100.0], [0.0, 10.0, -10.0, -10.0, 0.0]] + for idx, row in enumerate(report): + self.assertEqual( + expected_data[idx], + [row.invoiced, row.paid, row.outstanding, row.remaining_balance, row.future_amount], + ) + + def test_sales_person(self): + sales_person = ( + frappe.get_doc({"doctype": "Sales Person", "sales_person_name": "John Clark", "enabled": True}) + .insert() + .submit() + ) + si = self.create_sales_invoice(do_not_submit=True) + si.append("sales_team", {"sales_person": sales_person.name, "allocated_percentage": 100}) + si.save().submit() + + filters = { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "sales_person": sales_person.name, + "show_sales_person": True, + } + report = execute(filters)[1] + self.assertEqual(len(report), 1) + + expected_data = [100.0, 100.0, sales_person.name] + + row = report[0] + self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.sales_person]) + + def test_cost_center_filter(self): + si = self.create_sales_invoice() + filters = { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "cost_center": self.cost_center, + } + report = execute(filters)[1] + self.assertEqual(len(report), 1) + expected_data = [100.0, 100.0, self.cost_center] + row = report[0] + self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.cost_center]) + + def test_customer_group_filter(self): + si = self.create_sales_invoice() + cus_group = frappe.db.get_value("Customer", self.customer, "customer_group") + filters = { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "customer_group": cus_group, + } + report = execute(filters)[1] + self.assertEqual(len(report), 1) + expected_data = [100.0, 100.0, cus_group] + row = report[0] + self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.customer_group]) + + filters.update({"customer_group": "Individual"}) + report = execute(filters)[1] + self.assertEqual(len(report), 0) + + def test_party_account_filter(self): + si1 = self.create_sales_invoice() + self.customer2 = ( + frappe.get_doc( + { + "doctype": "Customer", + "customer_name": "Jane Doe", + "type": "Individual", + "default_currency": "USD", + } + ) + .insert() + .submit() + ) + + si2 = self.create_sales_invoice(do_not_submit=True) + si2.posting_date = add_days(today(), -1) + si2.customer = self.customer2 + si2.currency = "USD" + si2.conversion_rate = 80 + si2.debit_to = self.debtors_usd + si2.save().submit() + + # Filter on company currency receivable account + filters = { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "party_account": self.debit_to, + } + report = execute(filters)[1] + self.assertEqual(len(report), 1) + expected_data = [100.0, 100.0, self.debit_to, si1.currency] + row = report[0] + self.assertEqual( + expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency] + ) + + # Filter on USD receivable account + filters.update({"party_account": self.debtors_usd}) + report = execute(filters)[1] + self.assertEqual(len(report), 1) + expected_data = [8000.0, 8000.0, self.debtors_usd, si2.currency] + row = report[0] + self.assertEqual( + expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency] + ) + + # without filter on party account + filters.pop("party_account") + report = execute(filters)[1] + self.assertEqual(len(report), 2) + expected_data = [ + [8000.0, 8000.0, 100.0, 100.0, self.debtors_usd, si2.currency], + [100.0, 100.0, 100.0, 100.0, self.debit_to, si1.currency], + ] + for idx, row in enumerate(report): + self.assertEqual( + expected_data[idx], + [ + row.invoiced, + row.outstanding, + row.invoiced_in_account_currency, + row.outstanding_in_account_currency, + row.party_account, + row.account_currency, + ], + ) From e462edc6282f6e95b948f4f2583dabd96a92ca62 Mon Sep 17 00:00:00 2001 From: HENRY Florian Date: Fri, 25 Aug 2023 17:10:31 +0200 Subject: [PATCH 60/60] chore: update fr translation for Naming Series (#36785) * chore: update fr translation for Naming Series * chore: update fr translation * chore: update fr translation * chore: update fr translation --- erpnext/translations/fr.csv | 38 ++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv index 801604a4d8..37795261a9 100644 --- a/erpnext/translations/fr.csv +++ b/erpnext/translations/fr.csv @@ -3279,7 +3279,7 @@ Quality Feedback,Commentaires sur la qualité, Quality Feedback Template,Modèle de commentaires sur la qualité, Rules for applying different promotional schemes.,Règles d'application de différents programmes promotionnels., Show {0},Montrer {0}, -"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Caractères spéciaux sauf "-", "#", ".", "/", "{{" Et "}}" non autorisés dans les séries de nommage {0}", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Caractères spéciaux sauf "-", "#", ".", "/", "{{" Et "}}" non autorisés dans les masques de numérotation {0}", Target Details,Détails de la cible, {0} already has a Parent Procedure {1}.,{0} a déjà une procédure parent {1}., API,API, @@ -3292,7 +3292,7 @@ Group By,Par groupe, Invalid URL,URL invalide, Landscape,Paysage, Last Sync On,Dernière synchronisation le, -Naming Series,Nom de série, +Naming Series,Masque de numérotation, No data to export,Aucune donnée à exporter, Portrait,Portrait, Print Heading,Imprimer Titre, @@ -3962,7 +3962,7 @@ Please set {0},Veuillez définir {0},supplier Draft,Brouillon,"docstatus,=,0" Cancelled,Annulé,"docstatus,=,2" Please setup Instructor Naming System in Education > Education Settings,Veuillez configurer le système de dénomination de l'instructeur dans Éducation> Paramètres de l'éducation, -Please set Naming Series for {0} via Setup > Settings > Naming Series,Veuillez définir la série de noms pour {0} via Configuration> Paramètres> Série de noms, +Please set Naming Series for {0} via Setup > Settings > Naming Series,Veuillez définir le masque de numérotation pour {0} via Configuration> Paramètres> Série de noms, UOM Conversion factor ({0} -> {1}) not found for item: {2},Facteur de conversion UdM ({0} -> {1}) introuvable pour l'article: {2}, Item Code > Item Group > Brand,Code article> Groupe d'articles> Marque, Customer > Customer Group > Territory,Client> Groupe de clients> Territoire, @@ -3973,7 +3973,7 @@ Fetch Serial Numbers based on FIFO,Récupérer les numéros de série basés sur "Outward taxable supplies(other than zero rated, nil rated and exempted)","Fournitures taxables sortantes (autres que détaxées, nulles et exonérées)", "To allow different rates, disable the {0} checkbox in {1}.","Pour autoriser différents tarifs, désactivez la {0} case à cocher dans {1}.", Asset{} {assets_link} created for {},Élément {} {assets_link} créé pour {}, -Row {}: Asset Naming Series is mandatory for the auto creation for item {},Ligne {}: la série de noms d'éléments est obligatoire pour la création automatique de l'élément {}, +Row {}: Asset Naming Series is mandatory for the auto creation for item {},Ligne {}: Le masque de numérotation d'éléments est obligatoire pour la création automatique de l'élément {}, Assets not created for {0}. You will have to create asset manually.,Éléments non créés pour {0}. Vous devrez créer un actif manuellement., {0} {1} has accounting entries in currency {2} for company {3}. Please select a receivable or payable account with currency {2}.,{0} {1} a des écritures comptables dans la devise {2} pour l'entreprise {3}. Veuillez sélectionner un compte à recevoir ou à payer avec la devise {2}., Invalid Account,Compte invalide, @@ -3997,7 +3997,7 @@ Advanced Settings,Réglages avancés, Path,Chemin, Components,Composants, Verified By,Vérifié Par, -Invalid naming series (. missing) for {0},Série de noms non valide (. Manquante) pour {0}, +Invalid naming series (. missing) for {0},Masque de numérotation non valide (. Manquante) pour {0}, Filter Based On,Filtre basé sur, Reqd by date,Reqd par date, Manufacturer Part Number {0} is invalid,Le numéro de pièce du fabricant {0} n'est pas valide, @@ -5587,7 +5587,7 @@ Student Admission Program,Programme d'admission des étudiants, Minimum Age,Âge Minimum, Maximum Age,Âge Maximum, Application Fee,Frais de Dossier, -Naming Series (for Student Applicant),Nom de série (pour un candidat étudiant), +Naming Series (for Student Applicant),Masque de numérotation (pour un candidat étudiant), LMS Only,LMS seulement, EDU-APP-.YYYY.-,EDU-APP-YYYY.-, Application Date,Date de la Candidature, @@ -6074,7 +6074,7 @@ Hotel Reservation User,Utilisateur chargé des réservations d'hôtel, Hotel Room Reservation Item,Article de réservation de la chambre d'hôtel, Hotel Settings,Paramètres d'Hotel, Default Taxes and Charges,Taxes et frais par défaut, -Default Invoice Naming Series,Numéro de série par défaut pour les factures, +Default Invoice Naming Series,Masque de numérotation par défaut pour les factures, HR,RH, Date on which this component is applied,Date à laquelle ce composant est appliqué, Salary Slip,Fiche de Paie, @@ -7136,7 +7136,7 @@ Default Unit of Measure,Unité de Mesure par Défaut, Maintain Stock,Maintenir Stock, Standard Selling Rate,Prix de Vente Standard, Auto Create Assets on Purchase,Création automatique d'actifs à l'achat, -Asset Naming Series,Nom de série de l'actif, +Asset Naming Series,Masque de numérotation de l'actif, Over Delivery/Receipt Allowance (%),Surlivrance / indemnité de réception (%), Barcodes,Codes-barres, Shelf Life In Days,Durée de conservation en jours, @@ -7155,7 +7155,7 @@ Serial Nos and Batches,N° de Série et Lots, Has Batch No,A un Numéro de Lot, Automatically Create New Batch,Créer un Nouveau Lot Automatiquement, Batch Number Series,Série de numéros de lots, -"Example: ABCD.#####. If series is set and Batch No is not mentioned in transactions, then automatic batch number will be created based on this series. If you always want to explicitly mention Batch No for this item, leave this blank. Note: this setting will take priority over the Naming Series Prefix in Stock Settings.","Exemple: ABCD. #####. Si la série est définie et que le numéro de lot n'est pas mentionné dans les transactions, un numéro de lot sera automatiquement créé en avec cette série. Si vous préferez mentionner explicitement et systématiquement le numéro de lot pour cet article, laissez ce champ vide. Remarque: ce paramètre aura la priorité sur le préfixe de la série dans les paramètres de stock.", +"Example: ABCD.#####. If series is set and Batch No is not mentioned in transactions, then automatic batch number will be created based on this series. If you always want to explicitly mention Batch No for this item, leave this blank. Note: this setting will take priority over the Naming Series Prefix in Stock Settings.","Exemple: ABCD. #####. Si le masque est définie et que le numéro de lot n'est pas mentionné dans les transactions, un numéro de lot sera automatiquement créé en avec ce masque. Si vous préferez mentionner explicitement et systématiquement le numéro de lot pour cet article, laissez ce champ vide. Remarque: ce paramètre aura la priorité sur le préfixe du masque dans les paramètres de stock.", Has Expiry Date,A une date d'expiration, Retain Sample,Conserver l'échantillon, Max Sample Quantity,Quantité maximum d'échantillon, @@ -7455,8 +7455,8 @@ Inter Warehouse Transfer Settings,Paramètres de transfert entre entrepôts, Freeze Stock Entries,Geler les Entrées de Stocks, Stock Frozen Upto,Stock Gelé Jusqu'au, Batch Identification,Identification par lots, -Use Naming Series,Utiliser la série de noms, -Naming Series Prefix,Préfix du nom de série, +Use Naming Series,Utiliser le masque de numérotation, +Naming Series Prefix,Préfix du masque de numérotation, UOM Category,Catégorie d'unité de mesure (UdM), UOM Conversion Detail,Détails de Conversion de l'UdM, Variant Field,Champ de Variante, @@ -7914,7 +7914,7 @@ Is Inter State,Est Inter State, Purchase Details,Détails d'achat, Depreciation Posting Date,Date comptable de l'amortissement, "By default, the Supplier Name is set as per the Supplier Name entered. If you want Suppliers to be named by a ","Par défaut, le nom du fournisseur est défini selon le nom du fournisseur saisi. Si vous souhaitez que les fournisseurs soient nommés par un", - choose the 'Naming Series' option.,choisissez l'option 'Naming Series'., + choose the 'Naming Series' option.,choisissez l'option 'Masque de numérotation'., Configure the default Price List when creating a new Purchase transaction. Item prices will be fetched from this Price List.,Configurez la liste de prix par défaut lors de la création d'une nouvelle transaction d'achat. Les prix des articles seront extraits de cette liste de prix., "If this option is configured 'Yes', ERPNext will prevent you from creating a Purchase Invoice or Receipt without creating a Purchase Order first. This configuration can be overridden for a particular supplier by enabling the 'Allow Purchase Invoice Creation Without Purchase Order' checkbox in the Supplier master.","Si cette option est configurée «Oui», ERPNext vous empêchera de créer une facture d'achat ou un reçu sans créer d'abord une Commande d'Achat. Cette configuration peut être remplacée pour un fournisseur particulier en cochant la case «Autoriser la création de facture d'achat sans commmande d'achat» dans la fiche fournisseur.", "If this option is configured 'Yes', ERPNext will prevent you from creating a Purchase Invoice without creating a Purchase Receipt first. This configuration can be overridden for a particular supplier by enabling the 'Allow Purchase Invoice Creation Without Purchase Receipt' checkbox in the Supplier master.","Si cette option est configurée «Oui», ERPNext vous empêchera de créer une facture d'achat sans créer d'abord un reçu d'achat. Cette configuration peut être remplacée pour un fournisseur particulier en cochant la case "Autoriser la création de facture d'achat sans reçu d'achat" dans la fiche fournisseur.", @@ -8871,7 +8871,7 @@ Auto Insert Item Price If Missing,Création du prix de l'article dans les listes Update Existing Price List Rate,Mise a jour automatique du prix dans les listes de prix, Show Barcode Field in Stock Transactions,Afficher le champ Code Barre dans les transactions de stock, Convert Item Description to Clean HTML in Transactions,Convertir les descriptions d'articles en HTML valide lors des transactions, -Have Default Naming Series for Batch ID?,Nom de série par défaut pour les Lots ou Séries, +Have Default Naming Series for Batch ID?,Masque de numérotation par défaut pour les Lots ou Séries, "The percentage you are allowed to transfer more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed transfer 110 units","Le pourcentage de quantité que vous pourrez réceptionner en plus de la quantité commandée. Par exemple, vous avez commandé 100 unités, votre pourcentage de dépassement est de 10%, vous pourrez réceptionner 110 unités" Allowed Items,Articles autorisés, Party Specific Item,Restriction d'article disponible, @@ -8925,3 +8925,15 @@ Enable Reviews and Ratings,Activer les avis et notes, Enable Recommendations,Activer les recommendations, Item Search Settings,Paramétrage de la recherche d'article, Purchase demande,Demande de materiel, +Internal Customer,Client interne +Internal Supplier,Fournisseur interne +Contact & Address,Contact et Adresse +Primary Address and Contact,Adresse et contact principal +Supplier Primary Contact,Contact fournisseur principal +Supplier Primary Address,Adresse fournisseur principal +From Opportunity,Depuis l'opportunité +Default Receivable Accounts,Compte de débit par défaut +Receivable Accounts,Compte de débit +Mention if a non-standard receivable account,Veuillez mentionner s'il s'agit d'un compte débiteur non standard +Allow Purchase,Autoriser à l'achat +Inventory Settings,Paramétrage de l'inventaire