From 80c85dd17cf0c0fdcf07d2e0c2151b2852a0c611 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 12 Aug 2021 15:39:07 +0530 Subject: [PATCH 001/192] fix: Account currency validation for first transaction --- erpnext/controllers/accounts_controller.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a09290567e..9641d82501 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -14,7 +14,7 @@ from erpnext.accounts.utils import get_fiscal_years, validate_fiscal_year, get_a from erpnext.utilities.transaction_base import TransactionBase from erpnext.buying.utils import update_last_purchase_rate from erpnext.controllers.sales_and_purchase_return import validate_return -from erpnext.accounts.party import get_party_account_currency, validate_party_frozen_disabled +from erpnext.accounts.party import get_party_account_currency, validate_party_frozen_disabled, get_party_gle_currency from erpnext.accounts.doctype.pricing_rule.utils import (apply_pricing_rule_on_transaction, apply_pricing_rule_for_free_items, get_applied_pricing_rules) from erpnext.exceptions import InvalidCurrency @@ -113,6 +113,7 @@ class AccountsController(TransactionBase): self.validate_party() self.validate_currency() + self.validate_party_account_currency() if self.doctype == 'Purchase Invoice': self.calculate_paid_amount() @@ -1030,6 +1031,19 @@ class AccountsController(TransactionBase): # at quotation / sales order level and we shouldn't stop someone # from creating a sales invoice if sales order is already created + def validate_party_account_currency(self): + if self.doctype not in ('Sales Invoice', 'Purchase Invoice'): + return + + party_type, party = self.get_party() + party_gle_currency = get_party_gle_currency(party_type, party, self.company) + party_account = self.get('debit_to') if self.doctype == 'Sales Invoice' else self.get('credit_to') + party_account_currency = get_account_currency(party_account) + + if not party_gle_currency and (party_account_currency != self.currency): + frappe.throw(_("Party Account {0} currency and document currency should be same").format(frappe.bold(party_account))) + + def delink_advance_entries(self, linked_doc_name): total_allocated_amount = 0 for adv in self.advances: From f00620a3ca8d507dc947b82abdb5a90c6759bde5 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 15 Aug 2021 21:18:13 +0530 Subject: [PATCH 002/192] fix: Add party account validation for journal entry --- .../accounts/doctype/journal_entry/journal_entry.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 937597bc55..44a0182703 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -37,6 +37,7 @@ class JournalEntry(AccountsController): self.validate_party() self.validate_entries_for_advance() self.validate_multi_currency() + self.validate_party_account_currency() self.set_amounts_in_company_currency() self.validate_debit_credit_amount() @@ -433,6 +434,18 @@ class JournalEntry(AccountsController): self.set_exchange_rate() + def validate_party_account_currency(self): + for d in self.get("accounts"): + if self.party_type not in ('Customer', 'Supplier'): + continue + + party_gle_currency = get_party_gle_currency(self.party_type, self.party, self.company) + party_account_currency = get_account_currency(d.account) + + if not party_gle_currency and (party_account_currency != self.currency): + frappe.throw(_("Row {0}: Party Account {1} currency and document currency should be same").format( + frappe.bold(d.idx), frappe.bold(d.account))) + def set_amounts_in_company_currency(self): for d in self.get("accounts"): d.debit_in_account_currency = flt(d.debit_in_account_currency, d.precision("debit_in_account_currency")) From bcaf4752952f4aa7819c057ce61c8bd2ef69df78 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 15 Aug 2021 21:19:18 +0530 Subject: [PATCH 003/192] fix: Healthcare module accounting test cases --- erpnext/healthcare/doctype/lab_test/test_lab_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/healthcare/doctype/lab_test/test_lab_test.py b/erpnext/healthcare/doctype/lab_test/test_lab_test.py index c9f0029ed8..c3847ea3d1 100644 --- a/erpnext/healthcare/doctype/lab_test/test_lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/test_lab_test.py @@ -147,6 +147,7 @@ def create_sales_invoice(): sales_invoice.customer = frappe.db.get_value('Patient', patient, 'customer') sales_invoice.due_date = getdate() sales_invoice.company = '_Test Company' + sales_invoice.currency = 'INR' sales_invoice.debit_to = get_receivable_account('_Test Company') tests = [insulin_resistance_template, blood_test_template] From 0a618817dc76e7da00e8ae16521bf554b5fd9704 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 16 Aug 2021 10:40:26 +0530 Subject: [PATCH 004/192] Revert "fix: Add party account validation for journal entry" This reverts commit f00620a3ca8d507dc947b82abdb5a90c6759bde5. --- .../accounts/doctype/journal_entry/journal_entry.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 44a0182703..937597bc55 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -37,7 +37,6 @@ class JournalEntry(AccountsController): self.validate_party() self.validate_entries_for_advance() self.validate_multi_currency() - self.validate_party_account_currency() self.set_amounts_in_company_currency() self.validate_debit_credit_amount() @@ -434,18 +433,6 @@ class JournalEntry(AccountsController): self.set_exchange_rate() - def validate_party_account_currency(self): - for d in self.get("accounts"): - if self.party_type not in ('Customer', 'Supplier'): - continue - - party_gle_currency = get_party_gle_currency(self.party_type, self.party, self.company) - party_account_currency = get_account_currency(d.account) - - if not party_gle_currency and (party_account_currency != self.currency): - frappe.throw(_("Row {0}: Party Account {1} currency and document currency should be same").format( - frappe.bold(d.idx), frappe.bold(d.account))) - def set_amounts_in_company_currency(self): for d in self.get("accounts"): d.debit_in_account_currency = flt(d.debit_in_account_currency, d.precision("debit_in_account_currency")) From 60915e874d9f466618b313be65023a62591d0f97 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 21 Aug 2021 23:05:48 +0530 Subject: [PATCH 005/192] test: Update test cases for currency change validation --- .../period_closing_voucher/test_period_closing_voucher.py | 6 ++++-- erpnext/controllers/accounts_controller.py | 3 +++ .../doctype/patient_appointment/patient_appointment.py | 3 +++ erpnext/healthcare/doctype/therapy_plan/therapy_plan.py | 3 +++ erpnext/non_profit/doctype/membership/membership.py | 3 ++- erpnext/non_profit/doctype/membership/test_membership.py | 2 +- 6 files changed, 16 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py index f17a5c51a0..18f9549fee 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py @@ -81,7 +81,8 @@ class TestPeriodClosingVoucher(unittest.TestCase): income_account="Sales - TPC", expense_account="Cost of Goods Sold - TPC", rate=400, - debit_to="Debtors - TPC" + debit_to="Debtors - TPC", + currency="USD" ) create_sales_invoice( company=company, @@ -89,7 +90,8 @@ class TestPeriodClosingVoucher(unittest.TestCase): income_account="Sales - TPC", expense_account="Cost of Goods Sold - TPC", rate=200, - debit_to="Debtors - TPC" + debit_to="Debtors - TPC", + currency="USD" ) pcv = frappe.get_doc({ diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a801245ee5..654e0ebca0 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1113,6 +1113,9 @@ class AccountsController(TransactionBase): if self.doctype not in ('Sales Invoice', 'Purchase Invoice'): return + if self.is_opening == 'Yes': + return + party_type, party = self.get_party() party_gle_currency = get_party_gle_currency(party_type, party, self.company) party_account = self.get('debit_to') if self.doctype == 'Sales Invoice' else self.get('credit_to') diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index cdd4ad39c8..12f2fe19e0 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -14,6 +14,7 @@ from frappe.core.doctype.sms_settings.sms_settings import send_sms from erpnext.hr.doctype.employee.employee import is_holiday from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_receivable_account, get_income_account from erpnext.healthcare.utils import check_fee_validity, get_service_item_and_practitioner_charge, manage_fee_validity +from erpnext import get_company_currency class PatientAppointment(Document): def validate(self): @@ -164,6 +165,8 @@ def create_sales_invoice(appointment_doc): sales_invoice = frappe.new_doc('Sales Invoice') sales_invoice.patient = appointment_doc.patient sales_invoice.customer = frappe.get_value('Patient', appointment_doc.patient, 'customer') + sales_invoice.currency = frappe.get_value('Customer', sales_invoice.customer, 'default_currency') \ + or get_company_currency(appointment_doc.currency) sales_invoice.appointment = appointment_doc.name sales_invoice.due_date = getdate() sales_invoice.company = appointment_doc.company diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py index e209660434..c29f6a0018 100644 --- a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py +++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document from frappe.utils import flt, today +from erpnext import get_company_currency class TherapyPlan(Document): def validate(self): @@ -73,6 +74,8 @@ def make_sales_invoice(reference_name, patient, company, therapy_plan_template): si.company = company si.patient = patient si.customer = frappe.db.get_value('Patient', patient, 'customer') + si.currency = frappe.get_value('Customer', si.customer, 'default_currency') \ + or get_company_currency(si.company) item = frappe.db.get_value('Therapy Plan Template', therapy_plan_template, 'linked_item') price_list, price_list_currency = frappe.db.get_values('Price List', {'selling': 1}, ['name', 'currency'])[0] diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index b584116df3..88284f8bf3 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -14,6 +14,7 @@ from frappe.utils import add_days, add_years, nowdate, getdate, add_months, get_ from erpnext.non_profit.doctype.member.member import create_member from frappe import _ import erpnext +from erpnext import get_company_currency class Membership(Document): def validate(self): @@ -176,7 +177,7 @@ def make_invoice(membership, member, plan, settings): "doctype": "Sales Invoice", "customer": member.customer, "debit_to": settings.membership_debit_account, - "currency": membership.currency, + "currency": membership.currency or get_company_currency(settings.company), "company": settings.company, "is_pos": 0, "items": [ diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py index 5ad2088fc3..5c876dfedf 100644 --- a/erpnext/non_profit/doctype/membership/test_membership.py +++ b/erpnext/non_profit/doctype/membership/test_membership.py @@ -80,7 +80,7 @@ def make_membership(member, payload={}): "member": member, "membership_status": "Current", "membership_type": "_rzpy_test_milythm", - "currency": "INR", + "currency": "USD", "paid": 1, "from_date": nowdate(), "amount": 100 From c10a22529c8b612ca31d2221700e45184426753d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 22 Aug 2021 18:05:24 +0530 Subject: [PATCH 006/192] test: fix property name --- .../doctype/patient_appointment/patient_appointment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index 12f2fe19e0..fb0bf993f2 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -166,7 +166,7 @@ def create_sales_invoice(appointment_doc): sales_invoice.patient = appointment_doc.patient sales_invoice.customer = frappe.get_value('Patient', appointment_doc.patient, 'customer') sales_invoice.currency = frappe.get_value('Customer', sales_invoice.customer, 'default_currency') \ - or get_company_currency(appointment_doc.currency) + or get_company_currency(appointment_doc.company) sales_invoice.appointment = appointment_doc.name sales_invoice.due_date = getdate() sales_invoice.company = appointment_doc.company From 30876a105ca9ebb7f51741068b50385ca5a915c5 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 22 Aug 2021 23:48:23 +0530 Subject: [PATCH 007/192] test: Set default currency for patient --- .../doctype/patient_appointment/test_patient_appointment.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py index 9dd4a2c73c..b18c8ee220 100644 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -206,6 +206,7 @@ def create_patient(): patient = frappe.new_doc('Patient') patient.first_name = '_Test Patient' patient.sex = 'Female' + patient.default_currency = 'INR' patient.save(ignore_permissions=True) patient = patient.name return patient From 417d6abcf48668d053b413dae577f7c775c08416 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 26 Aug 2021 17:13:36 +0530 Subject: [PATCH 008/192] fix: Party account validation in JV --- .../accounts/doctype/journal_entry/journal_entry.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 937597bc55..88e747b44d 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -13,6 +13,7 @@ from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amo from erpnext.accounts.doctype.invoice_discounting.invoice_discounting \ import get_party_account_based_on_invoice_discounting from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts +from erpnext.accounts.party import get_party_gle_currency from six import string_types, iteritems @@ -35,6 +36,7 @@ class JournalEntry(AccountsController): self.clearance_date = None self.validate_party() + self.validate_party_account_currency() self.validate_entries_for_advance() self.validate_multi_currency() self.set_amounts_in_company_currency() @@ -194,6 +196,16 @@ class JournalEntry(AccountsController): if account_type in ["Receivable", "Payable"]: if not (d.party_type and d.party): frappe.throw(_("Row {0}: Party Type and Party is required for Receivable / Payable account {1}").format(d.idx, d.account)) + + def validate_party_account_currency(self): + for d in self.get("accounts"): + if d.party_type in ('Customer', 'Supplier'): + party_gle_currency = get_party_gle_currency(d.party_type, d.party, self.company) + party_account_currency = get_account_currency(d.account) + party_currency = frappe.db.get_value(d.party_type, d.party, 'default_currency') + + if not party_gle_currency and (party_account_currency != party_currency): + frappe.throw(_("Party Account {0} currency and default party currency should be same").format(frappe.bold(d.account))) def check_credit_limit(self): customers = list(set(d.party for d in self.get("accounts") From e82609315054d220e72effa2d1a6d649af205aa9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 13 May 2022 14:08:22 +0530 Subject: [PATCH 009/192] fix: multiple entries for same payment term --- .../payment_terms_status_for_sales_order.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index cb22fb6a80..91f4a5e50a 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -187,8 +187,9 @@ def get_so_with_invoices(filters): .on(soi.parent == so.name) .join(ps) .on(ps.parent == so.name) + .select(so.name) + .distinct() .select( - so.name, so.customer, so.transaction_date.as_("submitted"), ifelse(datediff(ps.due_date, functions.CurDate()) < 0, "Overdue", "Unpaid").as_("status"), From 3c35c9b6aee0f7e199aa78837f3d11f225b5a2ae Mon Sep 17 00:00:00 2001 From: hrzzz Date: Sat, 14 May 2022 10:45:26 -0300 Subject: [PATCH 010/192] fix: correction of the calculation to the average value when there is a discount on the document and not on the items --- .../doctype/authorization_control/authorization_control.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/authorization_control/authorization_control.py b/erpnext/setup/doctype/authorization_control/authorization_control.py index 309658d260..cfe3d62b8c 100644 --- a/erpnext/setup/doctype/authorization_control/authorization_control.py +++ b/erpnext/setup/doctype/authorization_control/authorization_control.py @@ -135,8 +135,8 @@ class AuthorizationControl(TransactionBase): price_list_rate, base_rate = 0, 0 for d in doc_obj.get("items"): if d.base_rate: - price_list_rate += flt(d.base_price_list_rate) or flt(d.base_rate) - base_rate += flt(d.base_rate) + price_list_rate += (flt(d.base_price_list_rate) or flt(d.base_rate)) * flt(d.qty) + base_rate += flt(d.base_rate) * flt(d.qty) if doc_obj.get("discount_amount"): base_rate -= flt(doc_obj.discount_amount) From 5b8726405d54c2a0601d379dbc78482e19858cda Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 15 May 2022 21:10:52 +0530 Subject: [PATCH 011/192] fix: Remove validation from Journal Entry --- .../doctype/journal_entry/journal_entry.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 1ce927f529..d28c3a8687 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -16,7 +16,7 @@ from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import ( from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import ( get_party_tax_withholding_details, ) -from erpnext.accounts.party import get_party_account, get_party_gle_currency +from erpnext.accounts.party import get_party_account from erpnext.accounts.utils import ( get_account_currency, get_balance_on, @@ -48,7 +48,6 @@ class JournalEntry(AccountsController): self.clearance_date = None self.validate_party() - self.validate_party_account_currency() self.validate_entries_for_advance() self.validate_multi_currency() self.set_amounts_in_company_currency() @@ -343,20 +342,6 @@ class JournalEntry(AccountsController): ) ) - def validate_party_account_currency(self): - for d in self.get("accounts"): - if d.party_type in ("Customer", "Supplier"): - party_gle_currency = get_party_gle_currency(d.party_type, d.party, self.company) - party_account_currency = get_account_currency(d.account) - party_currency = frappe.db.get_value(d.party_type, d.party, "default_currency") - - if not party_gle_currency and (party_account_currency != party_currency): - frappe.throw( - _("Party Account {0} currency and default party currency should be same").format( - frappe.bold(d.account) - ) - ) - def check_credit_limit(self): customers = list( set( From 65232edfd5634cb270a20cab076d2ad3cc644e28 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 16 May 2022 10:52:58 +0530 Subject: [PATCH 012/192] test: Update test cases --- .../period_closing_voucher/test_period_closing_voucher.py | 7 ++++++- erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py index be669a959c..3b938ea1ca 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py @@ -79,6 +79,7 @@ class TestPeriodClosingVoucher(unittest.TestCase): rate=400, debit_to="Debtors - TPC", currency="USD", + customer="_Test Customer USD", ) create_sales_invoice( company=company, @@ -88,6 +89,7 @@ class TestPeriodClosingVoucher(unittest.TestCase): rate=200, debit_to="Debtors - TPC", currency="USD", + customer="_Test Customer USD", ) pcv = self.make_period_closing_voucher(submit=False) @@ -121,14 +123,17 @@ class TestPeriodClosingVoucher(unittest.TestCase): surplus_account = create_account() cost_center = create_cost_center("Test Cost Center 1") - create_sales_invoice( + si = create_sales_invoice( company=company, income_account="Sales - TPC", expense_account="Cost of Goods Sold - TPC", cost_center=cost_center, rate=400, debit_to="Debtors - TPC", + currency="USD", + customer="_Test Customer USD", ) + jv = make_journal_entry( account1="Cash - TPC", account2="Sales - TPC", diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 4cf19b4454..3bd0cd2e83 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -752,7 +752,7 @@ class TestPricingRule(unittest.TestCase): title="_Test Pricing Rule with Min Qty - 2", ) - si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1, currency="USD") + si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1) item = si.items[0] item.stock_qty = 1 si.save() From 9553319d871f5f5c1bc97d96bd1839e58ed4dd3d Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 16 May 2022 16:19:32 +0200 Subject: [PATCH 013/192] feat: show title field for Purchase Taxes and Charges --- .../purchase_taxes_and_charges_template.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json index b46d2e32f2..c36efb89a3 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json @@ -1,10 +1,12 @@ { + "actions": [], "allow_import": 1, "allow_rename": 1, "creation": "2013-01-10 16:34:08", "description": "Standard tax template that can be applied to all Purchase Transactions. This template can contain list of tax heads and also other expense heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Consider Tax or Charge for: In this section you can specify if the tax / charge is only for valuation (not a part of total) or only for total (does not add value to the item) or for both.\n10. Add or Deduct: Whether you want to add or deduct the tax.", "doctype": "DocType", "document_type": "Setup", + "engine": "InnoDB", "field_order": [ "title", "is_default", @@ -74,7 +76,8 @@ ], "icon": "fa fa-money", "idx": 1, - "modified": "2019-11-25 13:05:26.220275", + "links": [], + "modified": "2022-05-16 16:15:29.059370", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Taxes and Charges Template", @@ -103,6 +106,10 @@ "role": "Purchase User" } ], + "show_title_field_in_link": 1, + "sort_field": "modified", "sort_order": "DESC", + "states": [], + "title_field": "title", "track_changes": 1 } \ No newline at end of file From cad64f19b7ad41bfc8ab6c78c329118ffea2785f Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 16 May 2022 16:19:53 +0200 Subject: [PATCH 014/192] feat: show title field for Sales Taxes and Charges --- .../sales_taxes_and_charges_template.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json index 19781bdffa..408ecbf36d 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "allow_rename": 1, "creation": "2013-01-10 16:34:09", @@ -77,7 +78,8 @@ ], "icon": "fa fa-money", "idx": 1, - "modified": "2019-11-25 13:06:03.279099", + "links": [], + "modified": "2022-05-16 16:14:52.061672", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Taxes and Charges Template", @@ -113,7 +115,10 @@ "write": 1 } ], + "show_title_field_in_link": 1, "sort_field": "modified", "sort_order": "ASC", + "states": [], + "title_field": "title", "track_changes": 1 } \ No newline at end of file From f915a9cef72dcbe5601ed661fca87b9124b73ce2 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 17 May 2022 11:22:01 +0530 Subject: [PATCH 015/192] fix: discount ledger entry in case of multicurrency invoice (#31024) --- erpnext/controllers/accounts_controller.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 7761551227..c9c2ab17a9 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1130,11 +1130,10 @@ class AccountsController(TransactionBase): { "account": item.discount_account, "against": supplier_or_customer, - dr_or_cr: flt(discount_amount, item.precision("discount_amount")), - dr_or_cr - + "_in_account_currency": flt( + dr_or_cr: flt( discount_amount * self.get("conversion_rate"), item.precision("discount_amount") ), + dr_or_cr + "_in_account_currency": flt(discount_amount, item.precision("discount_amount")), "cost_center": item.cost_center, "project": item.project, }, @@ -1149,11 +1148,11 @@ class AccountsController(TransactionBase): { "account": income_or_expense_account, "against": supplier_or_customer, - rev_dr_cr: flt(discount_amount, item.precision("discount_amount")), - rev_dr_cr - + "_in_account_currency": flt( + rev_dr_cr: flt( discount_amount * self.get("conversion_rate"), item.precision("discount_amount") ), + rev_dr_cr + + "_in_account_currency": flt(discount_amount, item.precision("discount_amount")), "cost_center": item.cost_center, "project": item.project or self.project, }, From 867f2c6282310cf9f25a7ddb1e2e5dcd15a79e62 Mon Sep 17 00:00:00 2001 From: Deepak <36790711+dpk404@users.noreply.github.com> Date: Tue, 17 May 2022 11:52:52 +0530 Subject: [PATCH 016/192] fix: TypeError in add_indicator_for_multicompany (#31042) Minor fix in add_indicator_for_multicompany In case of multi-company transactions add (+) buttons in connection dashboard pf customer aren't being loaded due to TypeError (TypeError: e.dashboard.stats_area.removeClass is not a function) created by "frm.dashboard.stats_area.removeClass('hidden');" during the stats section creation. --- erpnext/public/js/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index e8db097653..01710f1e41 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -125,7 +125,7 @@ $.extend(erpnext.utils, { }, add_indicator_for_multicompany: function(frm, info) { - frm.dashboard.stats_area.removeClass('hidden'); + frm.dashboard.stats_area.show(); frm.dashboard.stats_area_row.addClass('flex'); frm.dashboard.stats_area_row.css('flex-wrap', 'wrap'); From 28fe4f3d54614a7a5714208cf0c3a4901fccb66a Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 17 May 2022 12:07:58 +0530 Subject: [PATCH 017/192] fix: unlink Attendance from Employee Checkins on cancellation (#31045) --- erpnext/hr/doctype/attendance/attendance.py | 30 +++++++++++++++++++ .../employee_checkin/test_employee_checkin.py | 11 +++++++ 2 files changed, 41 insertions(+) diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index e43d40ef56..f3cae8089c 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -32,6 +32,9 @@ class Attendance(Document): self.validate_employee_status() self.check_leave_record() + def on_cancel(self): + self.unlink_attendance_from_checkins() + def validate_attendance_date(self): date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining") @@ -127,6 +130,33 @@ class Attendance(Document): if not emp: frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee)) + def unlink_attendance_from_checkins(self): + EmployeeCheckin = frappe.qb.DocType("Employee Checkin") + linked_logs = ( + frappe.qb.from_(EmployeeCheckin) + .select(EmployeeCheckin.name) + .where(EmployeeCheckin.attendance == self.name) + .for_update() + .run(as_dict=True) + ) + + if linked_logs: + ( + frappe.qb.update(EmployeeCheckin) + .set("attendance", "") + .where(EmployeeCheckin.attendance == self.name) + ).run() + + frappe.msgprint( + msg=_("Unlinked Attendance record from Employee Checkins: {}").format( + ", ".join(get_link_to_form("Employee Checkin", log.name) for log in linked_logs) + ), + title=_("Unlinked logs"), + indicator="blue", + is_minimizable=True, + wide=True, + ) + def get_duplicate_attendance_record(employee, attendance_date, shift, name=None): attendance = frappe.qb.DocType("Attendance") diff --git a/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py b/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py index b603b3a622..eb81f7d67c 100644 --- a/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py +++ b/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py @@ -76,6 +76,17 @@ class TestEmployeeCheckin(FrappeTestCase): ) self.assertEqual(attendance_count, 1) + def test_unlink_attendance_on_cancellation(self): + employee = make_employee("test_mark_attendance_and_link_log@example.com") + logs = make_n_checkins(employee, 3) + + frappe.db.delete("Attendance", {"employee": employee}) + attendance = mark_attendance_and_link_log(logs, "Present", nowdate(), 8.2) + attendance.cancel() + + linked_logs = frappe.db.get_all("Employee Checkin", {"attendance": attendance.name}) + self.assertEquals(len(linked_logs), 0) + def test_calculate_working_hours(self): check_in_out_type = [ "Alternating entries as IN and OUT during the same shift", From e07ce6efe0afde1bdbade6cbed9f53ac0dd236f0 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 17 May 2022 17:39:45 +0530 Subject: [PATCH 018/192] fix: Job Card excess transfer behaviour - Block excess transfer of items if not allowed in settings - Behaviour made consistent with js behaviour (button disappears if not pending and not allowed in settings) - Test for same case --- .../doctype/job_card/job_card.py | 51 +++++++++++++++---- .../doctype/job_card/test_job_card.py | 33 +++++++++++- .../stock/doctype/stock_entry/stock_entry.py | 2 +- 3 files changed, 73 insertions(+), 13 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index a98fc94868..776f2d0b41 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -42,6 +42,10 @@ class JobCardCancelError(frappe.ValidationError): pass +class JobCardOverTransferError(frappe.ValidationError): + pass + + class JobCard(Document): def onload(self): excess_transfer = frappe.db.get_single_value( @@ -522,23 +526,50 @@ class JobCard(Document): }, ) - def set_transferred_qty_in_job_card(self, ste_doc): + def set_transferred_qty_in_job_card_item(self, ste_doc): + from frappe.query_builder.functions import Sum + + def _validate_over_transfer(row, transferred_qty): + "Block over transfer of items if not allowed in settings." + allow_excess = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer") + required_qty = frappe.db.get_value("Job Card Item", row.job_card_item, "required_qty") + is_excess = flt(transferred_qty) > flt(required_qty) + + if is_excess and not allow_excess: + frappe.throw( + _( + "Row #{0}: Cannot transfer more than Required Qty {1} for Item {2} against Job Card {3}" + ).format( + row.idx, frappe.bold(required_qty), frappe.bold(row.item_code), ste_doc.job_card + ), + title=_("Excess Transfer"), + exc=JobCardOverTransferError, + ) + for row in ste_doc.items: if not row.job_card_item: continue - qty = frappe.db.sql( - """ SELECT SUM(qty) from `tabStock Entry Detail` sed, `tabStock Entry` se - WHERE sed.job_card_item = %s and se.docstatus = 1 and sed.parent = se.name and - se.purpose = 'Material Transfer for Manufacture' - """, - (row.job_card_item), - )[0][0] + sed = frappe.qb.DocType("Stock Entry Detail") + se = frappe.qb.DocType("Stock Entry") + transferred_qty = ( + frappe.qb.from_(sed) + .join(se) + .on(sed.parent == se.name) + .select(Sum(sed.qty)) + .where( + (sed.job_card_item == row.job_card_item) + & (se.docstatus == 1) + & (se.purpose == "Material Transfer for Manufacture") + ) + ).run()[0][0] - frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(qty)) + _validate_over_transfer(row, transferred_qty) + + frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty)) def set_transferred_qty(self, update_status=False): - "Set total FG Qty for which RM was transferred." + "Set total FG Qty in Job Card for which RM was transferred." if not self.items: self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0 diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 4647ddf05f..d21e542f90 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -2,10 +2,14 @@ # See license.txt import frappe -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import random_string -from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError, OverlapError +from erpnext.manufacturing.doctype.job_card.job_card import ( + JobCardOverTransferError, + OperationMismatchError, + OverlapError, +) from erpnext.manufacturing.doctype.job_card.job_card import ( make_stock_entry as make_stock_entry_from_jc, ) @@ -25,6 +29,7 @@ class TestJobCard(FrappeTestCase): "test_job_card_multiple_materials_transfer", "test_job_card_excess_material_transfer", "test_job_card_partial_material_transfer", + "test_job_card_excess_material_transfer_block", ) if self._testMethodName in tests_that_skip_setup: @@ -165,6 +170,7 @@ class TestJobCard(FrappeTestCase): # transfer was made for 2 fg qty in first transfer Stock Entry self.assertEqual(transfer_entry_2.fg_completed_qty, 0) + @change_settings("Manufacturing Settings", {"job_card_excess_transfer": 1}) def test_job_card_excess_material_transfer(self): "Test transferring more than required RM against Job Card." make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=25, basic_rate=100) @@ -208,6 +214,29 @@ class TestJobCard(FrappeTestCase): # JC is Completed with excess transfer self.assertEqual(job_card.status, "Completed") + @change_settings("Manufacturing Settings", {"job_card_excess_transfer": 0}) + def test_job_card_excess_material_transfer_block(self): + make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=25, basic_rate=100) + make_stock_entry( + item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=15, basic_rate=100 + ) + + job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name}) + + # fully transfer both RMs + transfer_entry_1 = make_stock_entry_from_jc(job_card_name) + transfer_entry_1.insert() + transfer_entry_1.submit() + + # transfer extra qty of both RM due to previously damaged RM + transfer_entry_2 = make_stock_entry_from_jc(job_card_name) + # deliberately change 'For Quantity' + transfer_entry_2.fg_completed_qty = 1 + transfer_entry_2.items[0].qty = 5 + transfer_entry_2.items[1].qty = 3 + transfer_entry_2.insert() + self.assertRaises(JobCardOverTransferError, transfer_entry_2.submit) + def test_job_card_partial_material_transfer(self): "Test partial material transfer against Job Card" diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 890ac476a7..26e866034e 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1141,7 +1141,7 @@ class StockEntry(StockController): if self.job_card: job_doc = frappe.get_doc("Job Card", self.job_card) job_doc.set_transferred_qty(update_status=True) - job_doc.set_transferred_qty_in_job_card(self) + job_doc.set_transferred_qty_in_job_card_item(self) if self.work_order: pro_doc = frappe.get_doc("Work Order", self.work_order) From 65d55ea8fa9df485eb998a972e6b2baf5ea9a224 Mon Sep 17 00:00:00 2001 From: maharshivpatel <39730881+maharshivpatel@users.noreply.github.com> Date: Wed, 18 May 2022 11:52:31 +0530 Subject: [PATCH 019/192] fix(india): eway bill cancel api is disabled (#31055) --- erpnext/regional/india/e_invoice/einvoice.js | 67 ++++++-------------- erpnext/regional/india/e_invoice/utils.py | 14 ++-- 2 files changed, 28 insertions(+), 53 deletions(-) diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index ea56d07d6d..4748b265dc 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -149,58 +149,27 @@ erpnext.setup_einvoice_actions = (doctype) => { } if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) { - const fields = [ - { - "label": "Reason", - "fieldname": "reason", - "fieldtype": "Select", - "reqd": 1, - "default": "1-Duplicate", - "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] - }, - { - "label": "Remark", - "fieldname": "remark", - "fieldtype": "Data", - "reqd": 1 - } - ]; const action = () => { - const d = new frappe.ui.Dialog({ - title: __('Cancel E-Way Bill'), - fields: fields, - primary_action: function() { - const data = d.get_values(); - frappe.call({ - method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill', - args: { - doctype, - docname: name, - eway_bill: ewaybill, - reason: data.reason.split('-')[0], - remark: data.remark - }, - freeze: true, - callback: () => { - frappe.show_alert({ - message: __('E-Way Bill Cancelled successfully'), - indicator: 'green' - }, 7); - frm.reload_doc(); - d.hide(); - }, - error: () => { - frappe.show_alert({ - message: __('E-Way Bill was not Cancelled'), - indicator: 'red' - }, 7); - d.hide(); - } - }); + let message = __('Cancellation of e-way bill is currently not supported.') + ' '; + message += '

'; + message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.'); + + const dialog = frappe.msgprint({ + title: __('Update E-Way Bill Cancelled Status?'), + message: message, + indicator: 'orange', + primary_action: { + action: function() { + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill', + args: { doctype, docname: name }, + freeze: true, + callback: () => frm.reload_doc() && dialog.hide() + }); + } }, - primary_action_label: __('Submit') + primary_action_label: __('Yes') }); - d.show(); }; add_custom_button(__("Cancel E-Way Bill"), action); } diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index ed1002a129..7cc636e478 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -797,7 +797,8 @@ class GSPConnector: self.irn_details_url = self.base_url + "/enriched/ei/api/invoice/irn" self.generate_irn_url = self.base_url + "/enriched/ei/api/invoice" self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin" - self.cancel_ewaybill_url = self.base_url + "/enriched/ei/api/ewayapi" + # cancel_ewaybill_url will only work if user have bought ewb api from adaequare. + self.cancel_ewaybill_url = self.base_url + "/enriched/ewb/ewayapi?action=CANEWB" self.generate_ewaybill_url = self.base_url + "/enriched/ei/api/ewaybill" self.get_qrcode_url = self.base_url + "/enriched/ei/others/qr/image" @@ -1185,6 +1186,7 @@ class GSPConnector: headers = self.get_headers() data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4) headers["username"] = headers["user_name"] + del headers["user_name"] try: res = self.make_request("post", self.cancel_ewaybill_url, headers, data) if res.get("success"): @@ -1358,9 +1360,13 @@ def generate_eway_bill(doctype, docname, **kwargs): @frappe.whitelist() -def cancel_eway_bill(doctype, docname, eway_bill, reason, remark): - gsp_connector = GSPConnector(doctype, docname) - gsp_connector.cancel_eway_bill(eway_bill, reason, remark) +def cancel_eway_bill(doctype, docname): + # NOTE: cancel_eway_bill api is disabled by Adequare. + # gsp_connector = GSPConnector(doctype, docname) + # gsp_connector.cancel_eway_bill(eway_bill, reason, remark) + + frappe.db.set_value(doctype, docname, "ewaybill", "") + frappe.db.set_value(doctype, docname, "eway_bill_cancelled", 1) @frappe.whitelist() From 9fb7b49b435887d7e48505830389ede0d740ccff Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 18 May 2022 11:53:00 +0530 Subject: [PATCH 020/192] fix(india): error while parsing e-invoice (#31053) --- erpnext/regional/india/e_invoice/utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 7cc636e478..bcb3e4fb85 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -649,6 +649,8 @@ def make_einvoice(invoice): try: einvoice = safe_json_load(einvoice) einvoice = santize_einvoice_fields(einvoice) + except json.JSONDecodeError: + raise except Exception: show_link_to_error_log(invoice, einvoice) @@ -765,7 +767,9 @@ def safe_json_load(json_string): frappe.throw( _( "Error in input data. Please check for any special characters near following input:
{}" - ).format(snippet) + ).format(snippet), + title=_("Invalid JSON"), + exc=e, ) From b6e46eea80da79d57bfcebdfbe5831f68b7e60e3 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 18 May 2022 13:00:00 +0530 Subject: [PATCH 021/192] perf: `get_boms_in_bottom_up_order` - Create child-parent map once and fetch value from child key to get parents - Get parents recursively for a leaf node (get all ancestors) - Approx. 44 secs for 4lakh 70k boms --- erpnext/manufacturing/doctype/bom/bom.py | 92 +++++++++++++++++------- 1 file changed, 67 insertions(+), 25 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 220ce1dbd8..a828869c36 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1,11 +1,11 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt import functools import re -from collections import deque +from collections import defaultdict, deque from operator import itemgetter -from typing import List +from typing import List, Optional import frappe from frappe import _ @@ -1130,35 +1130,77 @@ def get_children(parent=None, is_root=False, **filters): return bom_items -def get_boms_in_bottom_up_order(bom_no=None): - def _get_parent(bom_no): - return frappe.db.sql_list( - """ - select distinct bom_item.parent from `tabBOM Item` bom_item - where bom_item.bom_no = %s and bom_item.docstatus=1 and bom_item.parenttype='BOM' - and exists(select bom.name from `tabBOM` bom where bom.name=bom_item.parent and bom.is_active=1) - """, - bom_no, - ) +def get_boms_in_bottom_up_order(bom_no: Optional[str] = None) -> List: + def _generate_child_parent_map(): + bom = frappe.qb.DocType("BOM") + bom_item = frappe.qb.DocType("BOM Item") - count = 0 - bom_list = [] - if bom_no: - bom_list.append(bom_no) - else: - # get all leaf BOMs - bom_list = frappe.db.sql_list( + bom_parents = ( + frappe.qb.from_(bom_item) + .join(bom) + .on(bom_item.parent == bom.name) + .select(bom_item.bom_no, bom_item.parent) + .where( + (bom_item.bom_no.isnotnull()) + & (bom_item.bom_no != "") + & (bom.docstatus == 1) + & (bom.is_active == 1) + & (bom_item.parenttype == "BOM") + ) + ).run(as_dict=True) + + child_parent_map = defaultdict(list) + for bom in bom_parents: + child_parent_map[bom.bom_no].append(bom.parent) + + return child_parent_map + + def _get_flat_parent_map(leaf, child_parent_map): + parents_list = [] + + def _get_parents(node, parents_list): + "Returns updated ancestors list." + first_parents = child_parent_map.get(node) # immediate parents of node + if not first_parents: # top most node + return parents_list + + parents_list.extend(first_parents) + parents_list = list(dict.fromkeys(parents_list).keys()) # remove duplicates + + for nth_node in first_parents: + # recursively find parents + parents_list = _get_parents(nth_node, parents_list) + + return parents_list + + parents_list = _get_parents(leaf, parents_list) + return parents_list + + def _get_leaf_boms(): + return frappe.db.sql_list( """select name from `tabBOM` bom where docstatus=1 and is_active=1 and not exists(select bom_no from `tabBOM Item` where parent=bom.name and ifnull(bom_no, '')!='')""" ) - while count < len(bom_list): - for child_bom in _get_parent(bom_list[count]): - if child_bom not in bom_list: - bom_list.append(child_bom) - count += 1 + bom_list = [] + if bom_no: + bom_list.append(bom_no) + else: + bom_list = _get_leaf_boms() + + child_parent_map = _generate_child_parent_map() + + for leaf_bom in bom_list: + # generate list recursively bottom to top + parent_list = _get_flat_parent_map(leaf_bom, child_parent_map) + + if not parent_list: + continue + + bom_list.extend(parent_list) + bom_list = list(dict.fromkeys(bom_list).keys()) # remove duplicates return bom_list From 81c82c8d535dc6ec4ca84eaf87872a31cf12ec4a Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 16 Mar 2022 10:31:34 +0530 Subject: [PATCH 022/192] fix(ux): inform the user about salary slip creation/submission happening in the background --- erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 54d56f9612..5937e81fed 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -174,9 +174,11 @@ class PayrollEntry(Document): } ) if len(employees) > 30: - frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=employees, args=args) + frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=employees, args=args, publish_progress=False) + frappe.msgprint(_("Salary Slip creation has been queued. It may take a few minutes."), + alert=True, indicator="orange") else: - create_salary_slips_for_employees(employees, args, publish_progress=False) + create_salary_slips_for_employees(employees, args, publish_progress=True) # since this method is called via frm.call this doc needs to be updated manually self.reload() @@ -209,6 +211,8 @@ class PayrollEntry(Document): frappe.enqueue( submit_salary_slips_for_employees, timeout=600, payroll_entry=self, salary_slips=ss_list ) + frappe.msgprint(_("Salary Slip submission has been queued. It may take a few minutes."), + alert=True, indicator="orange") else: submit_salary_slips_for_employees(self, ss_list, publish_progress=False) From 1de6b14d1541717a85599a3e1912f2050af05d6e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 19 May 2022 14:25:16 +0530 Subject: [PATCH 023/192] fix: remove "scrap %" field (#31069) This does nothing, there's scrap items table below that's actually used for specifying scrap. --- erpnext/manufacturing/doctype/bom/bom.js | 8 ++------ erpnext/manufacturing/doctype/bom_item/bom_item.json | 12 +----------- .../report/bom_explorer/bom_explorer.py | 4 +--- 3 files changed, 4 insertions(+), 20 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 8a7634e24e..3d96f9c9c7 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -499,15 +499,11 @@ cur_frm.cscript.qty = function(doc) { cur_frm.cscript.rate = function(doc, cdt, cdn) { var d = locals[cdt][cdn]; - var scrap_items = false; - - if(cdt == 'BOM Scrap Item') { - scrap_items = true; - } + const is_scrap_item = cdt == "BOM Scrap Item"; if (d.bom_no) { frappe.msgprint(__("You cannot change the rate if BOM is mentioned against any Item.")); - get_bom_material_detail(doc, cdt, cdn, scrap_items); + get_bom_material_detail(doc, cdt, cdn, is_scrap_item); } else { erpnext.bom.calculate_rm_cost(doc); erpnext.bom.calculate_scrap_materials_cost(doc); diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.json b/erpnext/manufacturing/doctype/bom_item/bom_item.json index 3406215cbb..0a8ae7b4a7 100644 --- a/erpnext/manufacturing/doctype/bom_item/bom_item.json +++ b/erpnext/manufacturing/doctype/bom_item/bom_item.json @@ -33,7 +33,6 @@ "amount", "base_amount", "section_break_18", - "scrap", "qty_consumed_per_unit", "section_break_27", "has_variants", @@ -223,15 +222,6 @@ "fieldname": "section_break_18", "fieldtype": "Section Break" }, - { - "columns": 1, - "fieldname": "scrap", - "fieldtype": "Float", - "label": "Scrap %", - "oldfieldname": "scrap", - "oldfieldtype": "Currency", - "print_hide": 1 - }, { "fieldname": "qty_consumed_per_unit", "fieldtype": "Float", @@ -298,7 +288,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-01-24 16:57:57.020232", + "modified": "2022-05-19 02:32:43.785470", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Item", diff --git a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py index ac2f61c5de..2aa31be0f0 100644 --- a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py +++ b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py @@ -21,7 +21,7 @@ def get_exploded_items(bom, data, indent=0, qty=1): exploded_items = frappe.get_all( "BOM Item", filters={"parent": bom}, - fields=["qty", "bom_no", "qty", "scrap", "item_code", "item_name", "description", "uom"], + fields=["qty", "bom_no", "qty", "item_code", "item_name", "description", "uom"], ) for item in exploded_items: @@ -37,7 +37,6 @@ def get_exploded_items(bom, data, indent=0, qty=1): "qty": item.qty * qty, "uom": item.uom, "description": item.description, - "scrap": item.scrap, } ) if item.bom_no: @@ -64,5 +63,4 @@ def get_columns(): "fieldname": "description", "width": 150, }, - {"label": _("Scrap"), "fieldtype": "data", "fieldname": "scrap", "width": 100}, ] From 6d6616dbcdfec4171022bac9c0538c04a1fa5cd5 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 19 May 2022 14:25:47 +0530 Subject: [PATCH 024/192] fix: always update item_name for stock entry (#31068) If item_name is already set and for some reason becomes outdated then it's not updated in backend. Fix: always set item_name and stock_uom when fetching item details --- .../stock/doctype/stock_entry/stock_entry.py | 24 +++++++++---------- .../doctype/stock_entry/test_stock_entry.py | 18 ++++++++++++-- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 890ac476a7..5c35ed6c01 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -298,19 +298,17 @@ class StockEntry(StockController): for_update=True, ) - for f in ( - "uom", - "stock_uom", - "description", - "item_name", - "expense_account", - "cost_center", - "conversion_factor", - ): - if f == "stock_uom" or not item.get(f): - item.set(f, item_details.get(f)) - if f == "conversion_factor" and item.uom == item_details.get("stock_uom"): - item.set(f, item_details.get(f)) + reset_fields = ("stock_uom", "item_name") + for field in reset_fields: + item.set(field, item_details.get(field)) + + update_fields = ("uom", "description", "expense_account", "cost_center", "conversion_factor") + + for field in update_fields: + if not item.get(field): + item.set(field, item_details.get(field)) + if field == "conversion_factor" and item.uom == item_details.get("stock_uom"): + item.set(field, item_details.get(field)) if not item.transfer_qty and item.qty: item.transfer_qty = flt( diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 71baf9f53f..6f4c910c7f 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -2,8 +2,6 @@ # License: GNU General Public License v3. See license.txt -import unittest - import frappe from frappe.permissions import add_user_permission, remove_user_permission from frappe.tests.utils import FrappeTestCase, change_settings @@ -12,6 +10,7 @@ from frappe.utils import flt, nowdate, nowtime from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.stock.doctype.item.test_item import ( create_item, + make_item, make_item_variant, set_item_variant_settings, ) @@ -1443,6 +1442,21 @@ class TestStockEntry(FrappeTestCase): self.assertEqual(mapped_se.items[0].basic_rate, 100) self.assertEqual(mapped_se.items[0].basic_amount, 200) + def test_stock_entry_item_details(self): + item = make_item() + + se = make_stock_entry( + item_code=item.name, qty=1, to_warehouse="_Test Warehouse - _TC", do_not_submit=True + ) + + self.assertEqual(se.items[0].item_name, item.item_name) + se.items[0].item_name = "wat" + se.items[0].stock_uom = "Kg" + se.save() + + self.assertEqual(se.items[0].item_name, item.item_name) + self.assertEqual(se.items[0].stock_uom, item.stock_uom) + def make_serialized_item(**args): args = frappe._dict(args) From 5932e9d78a70fc53f9f1c57ed201815f80960384 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 19 May 2022 20:22:13 +0530 Subject: [PATCH 025/192] fix: DB update child items, remove redundancy, fix perf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move `get_boms_in_bottom_up_order` in bom update tool’s file - Remove repeated rm cost update from `update_cost`. `calculate_cost` handles RM cost update - db_update children in `calculate_cost` optionally - Don’t call `update_exploded_items` and regenerate exploded items in `update_cost`. They will stay the same (except cost) --- erpnext/manufacturing/doctype/bom/bom.py | 121 ++---------------- .../bom_update_tool/bom_update_tool.py | 97 +++++++++++++- 2 files changed, 105 insertions(+), 113 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index a828869c36..047bcc5239 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -3,9 +3,9 @@ import functools import re -from collections import defaultdict, deque +from collections import deque from operator import itemgetter -from typing import List, Optional +from typing import List import frappe from frappe import _ @@ -383,35 +383,9 @@ class BOM(WebsiteGenerator): existing_bom_cost = self.total_cost - for d in self.get("items"): - if not d.item_code: - continue - - 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 rate: - d.rate = rate - d.amount = flt(d.rate) * flt(d.qty) - d.base_rate = flt(d.rate) * flt(self.conversion_rate) - d.base_amount = flt(d.amount) * flt(self.conversion_rate) - - if save: - d.db_update() - if self.docstatus == 1: self.flags.ignore_validate_update_after_submit = True - self.calculate_cost(update_hour_rate) + self.calculate_cost(save_updates=save, update_hour_rate=update_hour_rate) if save: self.db_update() @@ -613,11 +587,11 @@ class BOM(WebsiteGenerator): bom_list.reverse() return bom_list - def calculate_cost(self, update_hour_rate=False): + def calculate_cost(self, save_update=False, update_hour_rate=False): """Calculate bom totals""" self.calculate_op_cost(update_hour_rate) - self.calculate_rm_cost() - self.calculate_sm_cost() + self.calculate_rm_cost(save=save_update) + self.calculate_sm_cost(save=save_update) self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost self.base_total_cost = ( self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost @@ -659,7 +633,7 @@ class BOM(WebsiteGenerator): if update_hour_rate: row.db_update() - def calculate_rm_cost(self): + def calculate_rm_cost(self, save=False): """Fetch RM rate as per today's valuation rate and calculate totals""" total_rm_cost = 0 base_total_rm_cost = 0 @@ -674,11 +648,13 @@ class BOM(WebsiteGenerator): total_rm_cost += d.amount base_total_rm_cost += d.base_amount + if save: + d.db_update() self.raw_material_cost = total_rm_cost self.base_raw_material_cost = base_total_rm_cost - def calculate_sm_cost(self): + def calculate_sm_cost(self, save=False): """Fetch RM rate as per today's valuation rate and calculate totals""" total_sm_cost = 0 base_total_sm_cost = 0 @@ -693,6 +669,8 @@ class BOM(WebsiteGenerator): ) total_sm_cost += d.amount base_total_sm_cost += d.base_amount + if save: + d.db_update() self.scrap_material_cost = total_sm_cost self.base_scrap_material_cost = base_total_sm_cost @@ -1130,81 +1108,6 @@ def get_children(parent=None, is_root=False, **filters): return bom_items -def get_boms_in_bottom_up_order(bom_no: Optional[str] = None) -> List: - def _generate_child_parent_map(): - bom = frappe.qb.DocType("BOM") - bom_item = frappe.qb.DocType("BOM Item") - - bom_parents = ( - frappe.qb.from_(bom_item) - .join(bom) - .on(bom_item.parent == bom.name) - .select(bom_item.bom_no, bom_item.parent) - .where( - (bom_item.bom_no.isnotnull()) - & (bom_item.bom_no != "") - & (bom.docstatus == 1) - & (bom.is_active == 1) - & (bom_item.parenttype == "BOM") - ) - ).run(as_dict=True) - - child_parent_map = defaultdict(list) - for bom in bom_parents: - child_parent_map[bom.bom_no].append(bom.parent) - - return child_parent_map - - def _get_flat_parent_map(leaf, child_parent_map): - parents_list = [] - - def _get_parents(node, parents_list): - "Returns updated ancestors list." - first_parents = child_parent_map.get(node) # immediate parents of node - if not first_parents: # top most node - return parents_list - - parents_list.extend(first_parents) - parents_list = list(dict.fromkeys(parents_list).keys()) # remove duplicates - - for nth_node in first_parents: - # recursively find parents - parents_list = _get_parents(nth_node, parents_list) - - return parents_list - - parents_list = _get_parents(leaf, parents_list) - return parents_list - - def _get_leaf_boms(): - return frappe.db.sql_list( - """select name from `tabBOM` bom - where docstatus=1 and is_active=1 - and not exists(select bom_no from `tabBOM Item` - where parent=bom.name and ifnull(bom_no, '')!='')""" - ) - - bom_list = [] - if bom_no: - bom_list.append(bom_no) - else: - bom_list = _get_leaf_boms() - - child_parent_map = _generate_child_parent_map() - - for leaf_bom in bom_list: - # generate list recursively bottom to top - parent_list = _get_flat_parent_map(leaf_bom, child_parent_map) - - if not parent_list: - continue - - bom_list.extend(parent_list) - bom_list = list(dict.fromkeys(bom_list).keys()) # remove duplicates - - return bom_list - - def add_additional_cost(stock_entry, work_order): # Add non stock items cost in the additional cost stock_entry.additional_costs = [] diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py index b0e7da1201..5b073b7539 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -2,7 +2,8 @@ # For license information, please see license.txt import json -from typing import TYPE_CHECKING, Dict, Literal, Optional, Union +from collections import defaultdict +from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Union if TYPE_CHECKING: from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import BOMUpdateLog @@ -10,8 +11,6 @@ if TYPE_CHECKING: import frappe from frappe.model.document import Document -from erpnext.manufacturing.doctype.bom.bom import get_boms_in_bottom_up_order - class BOMUpdateTool(Document): pass @@ -47,7 +46,10 @@ def update_cost() -> None: """Updates Cost for all BOMs from bottom to top.""" bom_list = get_boms_in_bottom_up_order() for bom in bom_list: - frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True) + bom_doc = frappe.get_doc("BOM", bom) + bom_doc.calculate_cost(save_updates=True, update_hour_rate=True) + # bom_doc.update_exploded_items(save=True) #TODO: edit exploded items rate + bom_doc.db_update() def create_bom_update_log( @@ -67,3 +69,90 @@ def create_bom_update_log( "update_type": update_type, } ).submit() + + +def get_boms_in_bottom_up_order(bom_no: Optional[str] = None) -> List: + """ + Eg: Main BOM + |- Sub BOM 1 + |- Leaf BOM 1 + |- Sub BOM 2 + |- Leaf BOM 2 + Result: [Leaf BOM 1, Leaf BOM 2, Sub BOM 1, Sub BOM 2, Main BOM] + """ + leaf_boms = [] + if bom_no: + leaf_boms.append(bom_no) + else: + leaf_boms = _get_leaf_boms() + + child_parent_map = _generate_child_parent_map() + bom_list = leaf_boms.copy() + + for leaf_bom in leaf_boms: + parent_list = _get_flat_parent_map(leaf_bom, child_parent_map) + + if not parent_list: + continue + + bom_list.extend(parent_list) + bom_list = list(dict.fromkeys(bom_list).keys()) # remove duplicates + + return bom_list + + +def _generate_child_parent_map(): + bom = frappe.qb.DocType("BOM") + bom_item = frappe.qb.DocType("BOM Item") + + bom_parents = ( + frappe.qb.from_(bom_item) + .join(bom) + .on(bom_item.parent == bom.name) + .select(bom_item.bom_no, bom_item.parent) + .where( + (bom_item.bom_no.isnotnull()) + & (bom_item.bom_no != "") + & (bom.docstatus == 1) + & (bom.is_active == 1) + & (bom_item.parenttype == "BOM") + ) + ).run(as_dict=True) + + child_parent_map = defaultdict(list) + for bom in bom_parents: + child_parent_map[bom.bom_no].append(bom.parent) + + return child_parent_map + + +def _get_flat_parent_map(leaf, child_parent_map): + "Get ancestors at all levels of a leaf BOM." + parents_list = [] + + def _get_parents(node, parents_list): + "Returns recursively updated ancestors list." + first_parents = child_parent_map.get(node) # immediate parents of node + if not first_parents: # top most node + return parents_list + + parents_list.extend(first_parents) + parents_list = list(dict.fromkeys(parents_list).keys()) # remove duplicates + + for nth_node in first_parents: + # recursively find parents + parents_list = _get_parents(nth_node, parents_list) + + return parents_list + + parents_list = _get_parents(leaf, parents_list) + return parents_list + + +def _get_leaf_boms(): + return frappe.db.sql_list( + """select name from `tabBOM` bom + where docstatus=1 and is_active=1 + and not exists(select bom_no from `tabBOM Item` + where parent=bom.name and ifnull(bom_no, '')!='')""" + ) From 9dc30830887c3b6e303df6bb92ba1148799afea6 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 19 May 2022 20:33:48 +0530 Subject: [PATCH 026/192] fix: Call `calculate_cost` for Draft BOM and typo in argument --- erpnext/manufacturing/doctype/bom/bom.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 047bcc5239..399eb5a087 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -385,7 +385,9 @@ class BOM(WebsiteGenerator): if self.docstatus == 1: self.flags.ignore_validate_update_after_submit = True - self.calculate_cost(save_updates=save, update_hour_rate=update_hour_rate) + + self.calculate_cost(save_updates=save, update_hour_rate=update_hour_rate) + if save: self.db_update() @@ -587,11 +589,11 @@ class BOM(WebsiteGenerator): bom_list.reverse() return bom_list - def calculate_cost(self, save_update=False, update_hour_rate=False): + def calculate_cost(self, save_updates=False, update_hour_rate=False): """Calculate bom totals""" self.calculate_op_cost(update_hour_rate) - self.calculate_rm_cost(save=save_update) - self.calculate_sm_cost(save=save_update) + self.calculate_rm_cost(save=save_updates) + self.calculate_sm_cost(save=save_updates) self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost self.base_total_cost = ( self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost From ef8164f188005942409123ca6bc136ad29b3c503 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 19 May 2022 20:33:55 +0530 Subject: [PATCH 027/192] refactor: UX for Salary Slip creation and submission via Payroll Entry - Add status for Queued/Failed - log errors and show corrective actions in payroll entry --- .../doctype/payroll_entry/payroll_entry.js | 31 ++- .../doctype/payroll_entry/payroll_entry.json | 50 ++++- .../doctype/payroll_entry/payroll_entry.py | 208 ++++++++++++------ .../payroll_entry/payroll_entry_list.js | 18 ++ 4 files changed, 230 insertions(+), 77 deletions(-) create mode 100644 erpnext/payroll/doctype/payroll_entry/payroll_entry_list.js diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js index 62e183e59c..a33f7665bd 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js @@ -64,6 +64,32 @@ frappe.ui.form.on('Payroll Entry', { if (frm.custom_buttons) frm.clear_custom_buttons(); frm.events.add_context_buttons(frm); } + + if (frm.doc.status == "Failed" && frm.doc.error_message) { + const issue = `issue`; + let process = (cint(frm.doc.salary_slips_created)) ? "submission" : "creation"; + + frm.dashboard.set_headline( + __("Salary Slip {0} failed. You can resolve the {1} and retry {0}.", [process, issue]) + ); + + $("#jump_to_error").on("click", (e) => { + e.preventDefault(); + frappe.utils.scroll_to( + frm.get_field("error_message").$wrapper, + true, + 30 + ); + }); + } + + frappe.realtime.on("completed_salary_slip_creation", function() { + frm.reload_doc(); + }); + + frappe.realtime.on("completed_salary_slip_submission", function() { + frm.reload_doc(); + }); }, get_employee_details: function (frm) { @@ -88,7 +114,7 @@ frappe.ui.form.on('Payroll Entry', { doc: frm.doc, method: "create_salary_slips", callback: function () { - frm.refresh(); + frm.reload_doc(); frm.toolbar.refresh(); } }); @@ -97,7 +123,7 @@ frappe.ui.form.on('Payroll Entry', { add_context_buttons: function (frm) { if (frm.doc.salary_slips_submitted || (frm.doc.__onload && frm.doc.__onload.submitted_ss)) { frm.events.add_bank_entry_button(frm); - } else if (frm.doc.salary_slips_created) { + } else if (frm.doc.salary_slips_created && frm.doc.status != 'Queued') { frm.add_custom_button(__("Submit Salary Slip"), function () { submit_salary_slip(frm); }).addClass("btn-primary"); @@ -331,6 +357,7 @@ const submit_salary_slip = function (frm) { method: 'submit_salary_slips', args: {}, callback: function () { + frm.reload_doc(); frm.events.refresh(frm); }, doc: frm.doc, diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.json b/erpnext/payroll/doctype/payroll_entry/payroll_entry.json index 0444134aa4..17882eb5d9 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.json +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.json @@ -8,11 +8,11 @@ "engine": "InnoDB", "field_order": [ "section_break0", - "column_break0", "posting_date", "payroll_frequency", "company", "column_break1", + "status", "currency", "exchange_rate", "payroll_payable_account", @@ -41,11 +41,14 @@ "cost_center", "account", "payment_account", - "amended_from", "column_break_33", "bank_account", "salary_slips_created", - "salary_slips_submitted" + "salary_slips_submitted", + "failure_details_section", + "error_message", + "section_break_41", + "amended_from" ], "fields": [ { @@ -53,11 +56,6 @@ "fieldtype": "Section Break", "label": "Select Employees" }, - { - "fieldname": "column_break0", - "fieldtype": "Column Break", - "width": "50%" - }, { "default": "Today", "fieldname": "posting_date", @@ -231,6 +229,7 @@ "fieldtype": "Check", "hidden": 1, "label": "Salary Slips Created", + "no_copy": 1, "read_only": 1 }, { @@ -239,6 +238,7 @@ "fieldtype": "Check", "hidden": 1, "label": "Salary Slips Submitted", + "no_copy": 1, "read_only": 1 }, { @@ -284,15 +284,44 @@ "label": "Payroll Payable Account", "options": "Account", "reqd": 1 + }, + { + "collapsible": 1, + "collapsible_depends_on": "error_message", + "depends_on": "eval:doc.status=='Failed';", + "fieldname": "failure_details_section", + "fieldtype": "Section Break", + "label": "Failure Details" + }, + { + "depends_on": "eval:doc.status=='Failed';", + "fieldname": "error_message", + "fieldtype": "Small Text", + "label": "Error Message", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "section_break_41", + "fieldtype": "Section Break" + }, + { + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "Draft\nSubmitted\nCancelled\nQueued\nFailed", + "print_hide": 1, + "read_only": 1 } ], "icon": "fa fa-cog", "is_submittable": 1, "links": [], - "modified": "2020-12-17 15:13:17.766210", + "modified": "2022-03-16 12:45:21.662765", "modified_by": "Administrator", "module": "Payroll", "name": "Payroll Entry", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -308,5 +337,6 @@ } ], "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 5937e81fed..86be813b91 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -1,6 +1,7 @@ # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +import json import frappe from dateutil.relativedelta import relativedelta @@ -16,6 +17,7 @@ from frappe.utils import ( comma_and, date_diff, flt, + get_link_to_form, getdate, ) @@ -39,8 +41,10 @@ class PayrollEntry(Document): def validate(self): self.number_of_employees = len(self.employees) + self.set_status() def on_submit(self): + self.set_status(update=True) self.create_salary_slips() def before_submit(self): @@ -49,6 +53,15 @@ class PayrollEntry(Document): if self.validate_employee_attendance(): frappe.throw(_("Cannot Submit, Employees left to mark attendance")) + def set_status(self, status=None, update=True): + if not status: + status = {0: "Draft", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0] + + if update: + self.db_set("status", status) + else: + self.status = status + def validate_employee_details(self): emp_with_sal_slip = [] for employee_details in self.employees: @@ -77,6 +90,7 @@ class PayrollEntry(Document): ) self.db_set("salary_slips_created", 0) self.db_set("salary_slips_submitted", 0) + self.set_status(update=True) def get_emp_list(self): """ @@ -174,11 +188,21 @@ class PayrollEntry(Document): } ) if len(employees) > 30: - frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=employees, args=args, publish_progress=False) - frappe.msgprint(_("Salary Slip creation has been queued. It may take a few minutes."), - alert=True, indicator="orange") + self.db_set("status", "Queued") + frappe.enqueue( + create_salary_slips_for_employees, + timeout=600, + employees=employees, + args=args, + publish_progress=False, + ) + frappe.msgprint( + _("Salary Slip creation is queued. It may take a few minutes"), + alert=True, + indicator="blue", + ) else: - create_salary_slips_for_employees(employees, args, publish_progress=True) + create_salary_slips_for_employees(employees, args, publish_progress=False) # since this method is called via frm.call this doc needs to be updated manually self.reload() @@ -208,11 +232,19 @@ class PayrollEntry(Document): self.check_permission("write") ss_list = self.get_sal_slip_list(ss_status=0) if len(ss_list) > 30: + self.db_set("status", "Queued") frappe.enqueue( - submit_salary_slips_for_employees, timeout=600, payroll_entry=self, salary_slips=ss_list + submit_salary_slips_for_employees, + timeout=600, + payroll_entry=self, + salary_slips=ss_list, + publish_progress=False, + ) + frappe.msgprint( + _("Salary Slip submission is queued. It may take a few minutes"), + alert=True, + indicator="blue", ) - frappe.msgprint(_("Salary Slip submission has been queued. It may take a few minutes."), - alert=True, indicator="orange") else: submit_salary_slips_for_employees(self, ss_list, publish_progress=False) @@ -227,7 +259,11 @@ class PayrollEntry(Document): ) if not account: - frappe.throw(_("Please set account in Salary Component {0}").format(salary_component)) + frappe.throw( + _("Please set account in Salary Component {0}").format( + get_link_to_form("Salary Component", salary_component) + ) + ) return account @@ -784,37 +820,81 @@ def payroll_entry_has_bank_entries(name): return response +def log_payroll_failure(process, payroll_entry, error): + error_log = frappe.log_error( + title=_("Salary Slip {0} failed for Payroll Entry {1}").format(process, payroll_entry.name) + ) + message_log = frappe.message_log.pop() if frappe.message_log else str(error) + + try: + error_message = json.loads(message_log).get("message") + except Exception: + error_message = message_log + + error_message += "\n" + _("Check Error Log {0} for more details.").format( + get_link_to_form("Error Log", error_log.name) + ) + + payroll_entry.db_set({"error_message": error_message, "status": "Failed"}) + + def create_salary_slips_for_employees(employees, args, publish_progress=True): - salary_slips_exists_for = get_existing_salary_slips(employees, args) - count = 0 - salary_slips_not_created = [] - for emp in employees: - if emp not in salary_slips_exists_for: - args.update({"doctype": "Salary Slip", "employee": emp}) - ss = frappe.get_doc(args) - ss.insert() - count += 1 - if publish_progress: - frappe.publish_progress( - count * 100 / len(set(employees) - set(salary_slips_exists_for)), - title=_("Creating Salary Slips..."), - ) + try: + frappe.db.savepoint("salary_slip_creation") + payroll_entry = frappe.get_doc("Payroll Entry", args.payroll_entry) + salary_slips_exist_for = get_existing_salary_slips(employees, args) + count = 0 - else: - salary_slips_not_created.append(emp) + for emp in employees: + if emp not in salary_slips_exist_for: + args.update({"doctype": "Salary Slip", "employee": emp}) + frappe.get_doc(args).insert() - payroll_entry = frappe.get_doc("Payroll Entry", args.payroll_entry) - payroll_entry.db_set("salary_slips_created", 1) - payroll_entry.notify_update() + count += 1 + if publish_progress: + frappe.publish_progress( + count * 100 / len(set(employees) - set(salary_slips_exist_for)), + title=_("Creating Salary Slips..."), + ) - if salary_slips_not_created: + payroll_entry.db_set({"status": "Submitted", "salary_slips_created": 1}) + + if salary_slips_exist_for: + frappe.msgprint( + _( + "Salary Slips already exist for employees {}, and will not be processed by this payroll." + ).format(frappe.bold(", ".join(emp for emp in salary_slips_exist_for))), + title=_("Message"), + indicator="orange", + ) + + except Exception as e: + frappe.db.rollback(save_point="salary_slip_creation") + log_payroll_failure("creation", payroll_entry, e) + + finally: + frappe.db.commit() + frappe.publish_realtime("completed_salary_slip_creation") + + +def show_payroll_submission_status(submitted, not_submitted, salary_slip): + if not submitted and not not_submitted: frappe.msgprint( _( - "Salary Slips already exists for employees {}, and will not be processed by this payroll." - ).format(frappe.bold(", ".join([emp for emp in salary_slips_not_created]))), - title=_("Message"), - indicator="orange", + "No salary slip found to submit for the above selected criteria OR salary slip already submitted" + ) ) + return + + if submitted: + frappe.msgprint( + _("Salary Slip submitted for period from {0} to {1}").format( + salary_slip.start_date, salary_slip.end_date + ) + ) + + if not_submitted: + frappe.msgprint(_("Could not submit some Salary Slips")) def get_existing_salary_slips(employees, args): @@ -831,45 +911,43 @@ def get_existing_salary_slips(employees, args): def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progress=True): - submitted_ss = [] - not_submitted_ss = [] - frappe.flags.via_payroll_entry = True + try: + frappe.db.savepoint("salary_slip_submission") - count = 0 - for ss in salary_slips: - ss_obj = frappe.get_doc("Salary Slip", ss[0]) - if ss_obj.net_pay < 0: - not_submitted_ss.append(ss[0]) - else: - try: - ss_obj.submit() - submitted_ss.append(ss_obj) - except frappe.ValidationError: - not_submitted_ss.append(ss[0]) + submitted = [] + not_submitted = [] + frappe.flags.via_payroll_entry = True + count = 0 - count += 1 - if publish_progress: - frappe.publish_progress(count * 100 / len(salary_slips), title=_("Submitting Salary Slips...")) - if submitted_ss: - payroll_entry.make_accrual_jv_entry() - frappe.msgprint( - _("Salary Slip submitted for period from {0} to {1}").format(ss_obj.start_date, ss_obj.end_date) - ) + for entry in salary_slips: + salary_slip = frappe.get_doc("Salary Slip", entry[0]) + if salary_slip.net_pay < 0: + not_submitted.append(entry[0]) + else: + try: + salary_slip.submit() + submitted.append(salary_slip) + except frappe.ValidationError: + not_submitted.append(entry[0]) - payroll_entry.email_salary_slip(submitted_ss) + count += 1 + if publish_progress: + frappe.publish_progress(count * 100 / len(salary_slips), title=_("Submitting Salary Slips...")) - payroll_entry.db_set("salary_slips_submitted", 1) - payroll_entry.notify_update() + if submitted: + payroll_entry.make_accrual_jv_entry() + payroll_entry.email_salary_slip(submitted) + payroll_entry.db_set({"salary_slips_submitted": 1, "status": "Submitted"}) - if not submitted_ss and not not_submitted_ss: - frappe.msgprint( - _( - "No salary slip found to submit for the above selected criteria OR salary slip already submitted" - ) - ) + show_payroll_submission_status(submitted, not_submitted, salary_slip) - if not_submitted_ss: - frappe.msgprint(_("Could not submit some Salary Slips")) + except Exception as e: + frappe.db.rollback(save_point="salary_slip_submission") + log_payroll_failure("submission", payroll_entry, e) + + finally: + frappe.db.commit() + frappe.publish_realtime("completed_salary_slip_submission") frappe.flags.via_payroll_entry = False diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry_list.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry_list.js new file mode 100644 index 0000000000..56390b79d8 --- /dev/null +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry_list.js @@ -0,0 +1,18 @@ +// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +// License: GNU General Public License v3. See license.txt + +// render +frappe.listview_settings['Payroll Entry'] = { + has_indicator_for_draft: 1, + get_indicator: function(doc) { + var status_color = { + 'Draft': 'red', + 'Submitted': 'blue', + 'Queued': 'orange', + 'Failed': 'red', + 'Cancelled': 'red' + + }; + return [__(doc.status), status_color[doc.status], 'status,=,'+doc.status]; + } +}; From 7d4872aedd254efe85f61d88d98a3285859da11d Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 19 May 2022 20:35:56 +0530 Subject: [PATCH 028/192] patch: set payroll entry status --- erpnext/patches.txt | 1 + .../patches/v13_0/set_payroll_entry_status.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 erpnext/patches/v13_0/set_payroll_entry_status.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 4d9a7e06bf..e710aa3389 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -372,3 +372,4 @@ erpnext.patches.v14_0.discount_accounting_separation erpnext.patches.v14_0.delete_employee_transfer_property_doctype erpnext.patches.v13_0.create_accounting_dimensions_in_orders erpnext.patches.v13_0.set_per_billed_in_return_delivery_note +erpnext.patches.v13_0.set_payroll_entry_status diff --git a/erpnext/patches/v13_0/set_payroll_entry_status.py b/erpnext/patches/v13_0/set_payroll_entry_status.py new file mode 100644 index 0000000000..97adff9295 --- /dev/null +++ b/erpnext/patches/v13_0/set_payroll_entry_status.py @@ -0,0 +1,16 @@ +import frappe +from frappe.query_builder import Case + + +def execute(): + PayrollEntry = frappe.qb.DocType("Payroll Entry") + + ( + frappe.qb.update(PayrollEntry).set( + "status", + Case() + .when(PayrollEntry.docstatus == 0, "Draft") + .when(PayrollEntry.docstatus == 1, "Submitted") + .else_("Cancelled"), + ) + ).run() From 9a7e9d902d2b2960034cf2d7ad0d084dee01c1b4 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 19 May 2022 21:24:31 +0530 Subject: [PATCH 029/192] perf: Use cached doc instead of `get_doc` - Doc is only used to iterate over items(which wont change) and change rate/amount of rows - These changes are inserted in db via `db_update`, so no harm - Tested locally: refetching cached doc after db update, reflects fresh data. --- .../manufacturing/doctype/bom_update_tool/bom_update_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py index 5b073b7539..e765725340 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -46,7 +46,7 @@ def update_cost() -> None: """Updates Cost for all BOMs from bottom to top.""" bom_list = get_boms_in_bottom_up_order() for bom in bom_list: - bom_doc = frappe.get_doc("BOM", bom) + bom_doc = frappe.get_cached_doc("BOM", bom) bom_doc.calculate_cost(save_updates=True, update_hour_rate=True) # bom_doc.update_exploded_items(save=True) #TODO: edit exploded items rate bom_doc.db_update() From dd99c00eb64dc16b0f54a0cbf8e2c3b7f0f7e5fe Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 19 May 2022 21:48:24 +0530 Subject: [PATCH 030/192] fix: Get fresh RM rate in `calculate_rm_cost` --- erpnext/manufacturing/doctype/bom/bom.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 399eb5a087..560019a86d 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -641,6 +641,18 @@ class BOM(WebsiteGenerator): base_total_rm_cost = 0 for d in self.get("items"): + 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")) d.base_amount = d.amount * flt(self.conversion_rate) From 90d4dc0cd6428d2befa0805c9903446e668c9ba8 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 20 May 2022 01:02:56 +0530 Subject: [PATCH 031/192] fix: `test_work_order_with_non_stock_item` - Use the right price list and currency to avoid rate conversion (1000/62.9), since rates are reset correctly now - Use RM rate based on Price List in BOM. Non stock item has no valuation --- .../production_plan/test_production_plan.py | 1 - .../doctype/work_order/test_work_order.py | 15 ++++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 891a497878..e88049d810 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -798,7 +798,6 @@ def make_bom(**args): for item in args.raw_materials: item_doc = frappe.get_doc("Item", item) - bom.append( "items", { diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 2aba48231b..27e7e24a82 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -417,7 +417,7 @@ class TestWorkOrder(FrappeTestCase): "doctype": "Item Price", "item_code": "_Test FG Non Stock Item", "price_list_rate": 1000, - "price_list": "Standard Buying", + "price_list": "_Test Price List India", } ).insert(ignore_permissions=True) @@ -426,8 +426,17 @@ class TestWorkOrder(FrappeTestCase): item_code="_Test FG Item", target="_Test Warehouse - _TC", qty=1, basic_rate=100 ) - if not frappe.db.get_value("BOM", {"item": fg_item}): - make_bom(item=fg_item, rate=1000, raw_materials=["_Test FG Item", "_Test FG Non Stock Item"]) + if not frappe.db.get_value("BOM", {"item": fg_item, "docstatus": 1}): + bom = make_bom( + item=fg_item, + rate=1000, + raw_materials=["_Test FG Item", "_Test FG Non Stock Item"], + do_not_save=True, + ) + bom.rm_cost_as_per = "Price List" # non stock item won't have valuation rate + bom.buying_price_list = "_Test Price List India" + bom.currency = "INR" + bom.save() wo = make_wo_order_test_record(production_item=fg_item) From 3128f9603ed74d08855c367a4b75bdc76f56399b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 20 May 2022 08:31:37 +0530 Subject: [PATCH 032/192] fix: Loan repayment entries for payroll payable account --- .../doctype/loan_repayment/loan_repayment.py | 2 -- erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 10 ++++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 2535180092..c819de9b55 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -447,8 +447,6 @@ class LoanRepayment(AccountsController): "remarks": remarks, "cost_center": self.cost_center, "posting_date": getdate(self.posting_date), - "party_type": self.applicant_type if self.repay_from_salary else "", - "party": self.applicant if self.repay_from_salary else "", } ) ) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 54d56f9612..473fb0d7c7 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -16,6 +16,7 @@ from frappe.utils import ( comma_and, date_diff, flt, + get_link_to_form, getdate, ) @@ -45,6 +46,7 @@ class PayrollEntry(Document): def before_submit(self): self.validate_employee_details() + self.validate_payroll_payable_account() if self.validate_attendance: if self.validate_employee_attendance(): frappe.throw(_("Cannot Submit, Employees left to mark attendance")) @@ -66,6 +68,14 @@ class PayrollEntry(Document): if len(emp_with_sal_slip): frappe.throw(_("Salary Slip already exists for {0}").format(comma_and(emp_with_sal_slip))) + def validate_payroll_payable_account(self): + if frappe.db.get_value("Account", self.payroll_payable_account, "account_type"): + frappe.throw( + _( + "Account type cannot be set for payroll payable account {0}, please remove and try again" + ).format(frappe.bold(get_link_to_form("Account", self.payroll_payable_account))) + ) + def on_cancel(self): frappe.delete_doc( "Salary Slip", From c41f9f046fb987d942298db99b5b1cf64f8b7684 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 20 May 2022 15:33:03 +0530 Subject: [PATCH 033/192] fix(India): Async issue in company address trigger --- erpnext/regional/india/taxes.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js index 5f6dcdeb92..c0e6b91a1c 100644 --- a/erpnext/regional/india/taxes.js +++ b/erpnext/regional/india/taxes.js @@ -1,6 +1,7 @@ erpnext.setup_auto_gst_taxation = (doctype) => { frappe.ui.form.on(doctype, { company_address: function(frm) { + console.log("#########"); frm.trigger('get_tax_template'); }, shipping_address: function(frm) { @@ -22,6 +23,7 @@ erpnext.setup_auto_gst_taxation = (doctype) => { 'shipping_address': frm.doc.shipping_address || '', 'shipping_address_name': frm.doc.shipping_address_name || '', 'customer_address': frm.doc.customer_address || '', + 'company_address': frm.doc.company_address, 'supplier_address': frm.doc.supplier_address, 'customer': frm.doc.customer, 'supplier': frm.doc.supplier, @@ -39,6 +41,7 @@ erpnext.setup_auto_gst_taxation = (doctype) => { }, debounce: 2000, callback: function(r) { + console.log(r.message); if(r.message) { frm.set_value('taxes_and_charges', r.message.taxes_and_charges); frm.set_value('taxes', r.message.taxes); From 8fd0b3b9f50dfc7794b68fabb136d28c2913e196 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 20 May 2022 15:34:03 +0530 Subject: [PATCH 034/192] chore: Linting issues --- erpnext/regional/india/taxes.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js index c0e6b91a1c..88973e36b6 100644 --- a/erpnext/regional/india/taxes.js +++ b/erpnext/regional/india/taxes.js @@ -1,7 +1,6 @@ erpnext.setup_auto_gst_taxation = (doctype) => { frappe.ui.form.on(doctype, { company_address: function(frm) { - console.log("#########"); frm.trigger('get_tax_template'); }, shipping_address: function(frm) { @@ -41,7 +40,6 @@ erpnext.setup_auto_gst_taxation = (doctype) => { }, debounce: 2000, callback: function(r) { - console.log(r.message); if(r.message) { frm.set_value('taxes_and_charges', r.message.taxes_and_charges); frm.set_value('taxes', r.message.taxes); From 163085f2018b4231d126031dbaa0eaa84715ce21 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 9 May 2022 19:43:38 +0530 Subject: [PATCH 035/192] feat: payment ledger doctype created --- .../doctype/payment_ledger_entry/__init__.py | 0 .../payment_ledger_entry.js | 8 + .../payment_ledger_entry.json | 180 +++++++++++++++ .../payment_ledger_entry.py | 22 ++ .../test_payment_ledger_entry.py | 215 ++++++++++++++++++ 5 files changed, 425 insertions(+) create mode 100644 erpnext/accounts/doctype/payment_ledger_entry/__init__.py create mode 100644 erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.js create mode 100644 erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json create mode 100644 erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py create mode 100644 erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py diff --git a/erpnext/accounts/doctype/payment_ledger_entry/__init__.py b/erpnext/accounts/doctype/payment_ledger_entry/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.js b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.js new file mode 100644 index 0000000000..5a7be8e5ab --- /dev/null +++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.js @@ -0,0 +1,8 @@ +// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Payment Ledger Entry', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json new file mode 100644 index 0000000000..d96107678f --- /dev/null +++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json @@ -0,0 +1,180 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "format:PLE-{YY}-{MM}-{######}", + "creation": "2022-05-09 19:35:03.334361", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "posting_date", + "company", + "account_type", + "account", + "party_type", + "party", + "due_date", + "cost_center", + "finance_book", + "voucher_type", + "voucher_no", + "against_voucher_type", + "against_voucher_no", + "amount", + "account_currency", + "amount_in_account_currency", + "delinked" + ], + "fields": [ + { + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date" + }, + { + "fieldname": "account_type", + "fieldtype": "Select", + "label": "Account Type", + "options": "Receivable\nPayable" + }, + { + "fieldname": "account", + "fieldtype": "Link", + "label": "Account", + "options": "Account" + }, + { + "fieldname": "party_type", + "fieldtype": "Link", + "label": "Party Type", + "options": "DocType" + }, + { + "fieldname": "party", + "fieldtype": "Dynamic Link", + "label": "Party", + "options": "party_type" + }, + { + "fieldname": "voucher_type", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Voucher Type", + "options": "DocType" + }, + { + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Voucher No", + "options": "voucher_type" + }, + { + "fieldname": "against_voucher_type", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Against Voucher Type", + "options": "DocType" + }, + { + "fieldname": "against_voucher_no", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Against Voucher No", + "options": "against_voucher_type" + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "options": "Company:company:default_currency" + }, + { + "fieldname": "account_currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency" + }, + { + "fieldname": "amount_in_account_currency", + "fieldtype": "Currency", + "label": "Amount in Account Currency", + "options": "account_currency" + }, + { + "default": "0", + "fieldname": "delinked", + "fieldtype": "Check", + "in_list_view": 1, + "label": "DeLinked" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "due_date", + "fieldtype": "Date", + "label": "Due Date" + }, + { + "fieldname": "finance_book", + "fieldtype": "Link", + "label": "Finance Book", + "options": "Finance Book" + } + ], + "in_create": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2022-05-19 18:04:44.609115", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Payment Ledger Entry", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [ + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Auditor", + "share": 1 + } + ], + "search_fields": "voucher_no, against_voucher_no", + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py new file mode 100644 index 0000000000..43e19f4ae7 --- /dev/null +++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py @@ -0,0 +1,22 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + + +import frappe +from frappe import _ +from frappe.model.document import Document + + +class PaymentLedgerEntry(Document): + def validate_account(self): + valid_account = frappe.db.get_list( + "Account", + "name", + filters={"name": self.account, "account_type": self.account_type, "company": self.company}, + ignore_permissions=True, + ) + if not valid_account: + frappe.throw(_("{0} account is not of type {1}").format(self.account, self.account_type)) + + def validate(self): + self.validate_account() 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 new file mode 100644 index 0000000000..f874b75432 --- /dev/null +++ b/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py @@ -0,0 +1,215 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import unittest + +import frappe +from frappe import qb +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days, nowdate + +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.party import get_party_account +from erpnext.stock.doctype.item.test_item import create_item + + +# class TestPaymentLedgerEntry(FrappeTestCase): +class TestPaymentLedgerEntry(unittest.TestCase): + def setUp(self): + self.create_company() + self.create_item() + self.create_customer() + self.clear_old_entries() + + # def tearDown(self): + # frappe.db.rollback() + + def create_company(self): + company_name = "_Test Payment Ledger" + company = None + if frappe.db.exists("Company", company_name): + company = frappe.get_doc("Company", company_name) + else: + company = frappe.get_doc( + { + "doctype": "Company", + "company_name": company_name, + "country": "India", + "default_currency": "INR", + "create_chart_of_accounts_based_on": "Standard Template", + "chart_of_accounts": "Standard", + } + ) + company = company.save() + + self.company = company.name + self.cost_center = company.cost_center + self.warehouse = "All Warehouses - _PL" + self.income_account = "Sales - _PL" + self.expense_account = "Cost of Goods Sold - _PL" + self.debit_to = "Debtors - _PL" + self.creditors = "Creditors - _PL" + + # create bank account + if frappe.db.exists("Account", "HDFC - _PL"): + self.bank = "HDFC - _PL" + else: + bank_acc = frappe.get_doc( + { + "doctype": "Account", + "account_name": "HDFC", + "parent_account": "Bank Accounts - _PL", + "company": self.company, + } + ) + bank_acc.save() + self.bank = bank_acc.name + + def create_item(self): + item_name = "_Test PL Item" + item = create_item( + item_code=item_name, is_stock_item=0, company=self.company, warehouse=self.warehouse + ) + self.item = item if isinstance(item, str) else item.item_code + + def create_customer(self): + name = "_Test PL Customer" + if frappe.db.exists("Customer", name): + self.customer = name + else: + customer = frappe.new_doc("Customer") + customer.customer_name = name + customer.type = "Individual" + customer.save() + self.customer = customer.name + + def create_sales_invoice( + self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False + ): + """ + Helper function to populate default values in sales invoice + """ + sinv = create_sales_invoice( + qty=qty, + rate=rate, + company=self.company, + customer=self.customer, + item_code=self.item, + item_name=self.item, + cost_center=self.cost_center, + warehouse=self.warehouse, + debit_to=self.debit_to, + parent_cost_center=self.cost_center, + update_stock=0, + currency="INR", + is_pos=0, + is_return=0, + return_against=None, + income_account=self.income_account, + expense_account=self.expense_account, + do_not_save=do_not_save, + do_not_submit=do_not_submit, + ) + return sinv + + def create_payment_entry(self, amount=100, posting_date=nowdate()): + """ + Helper function to populate default values in payment entry + """ + payment = create_payment_entry( + company=self.company, + payment_type="Receive", + party_type="Customer", + party=self.customer, + paid_from=self.debit_to, + paid_to=self.bank, + paid_amount=amount, + ) + payment.posting_date = posting_date + return payment + + 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() + + def create_journal_entry( + self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None + ): + je = frappe.new_doc("Journal Entry") + je.posting_date = posting_date or nowdate() + je.company = self.company + je.user_remark = "test" + if not cost_center: + cost_center = self.cost_center + je.set( + "accounts", + [ + { + "account": acc1, + "cost_center": cost_center, + "debit_in_account_currency": amount if amount > 0 else 0, + "credit_in_account_currency": abs(amount) if amount < 0 else 0, + }, + { + "account": acc2, + "cost_center": cost_center, + "credit_in_account_currency": amount if amount > 0 else 0, + "debit_in_account_currency": abs(amount) if amount < 0 else 0, + }, + ], + ) + return je + + def test_create_all_types(self): + transaction_date = nowdate() + amount = 100 + # full payment using PE + si1 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date) + pe2 = get_payment_entry(si1.doctype, si1.name).save().submit() + + # partial payment of invoice using PE + si2 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date) + pe2 = get_payment_entry(si2.doctype, si2.name) + pe2.get("references")[0].allocated_amount = 50 + pe2.get("references")[0].outstanding_amount = 50 + pe2 = pe2.save().submit() + + # reconcile against return invoice + si3 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date) + cr_note1 = self.create_sales_invoice( + qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True + ) + cr_note1.is_return = 1 + cr_note1.return_against = si3.name + cr_note1 = cr_note1.save().submit() + + # reconcile against return invoice using JE + si4 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date) + cr_note2 = self.create_sales_invoice( + qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True + ) + cr_note2.is_return = 1 + cr_note2 = cr_note2.save().submit() + je1 = self.create_journal_entry( + self.debit_to, self.debit_to, amount, posting_date=transaction_date + ) + je1.get("accounts")[0].party_type = je1.get("accounts")[1].party_type = "Customer" + je1.get("accounts")[0].party = je1.get("accounts")[1].party = self.customer + je1.get("accounts")[0].reference_type = cr_note2.doctype + je1.get("accounts")[0].reference_name = cr_note2.name + je1.get("accounts")[1].reference_type = si4.doctype + je1.get("accounts")[1].reference_name = si4.name + je1 = je1.save().submit() + + def test_dummy(self): + pass From 8e72f19bfb298c578979cc509f3b892e493d39d1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 16 May 2022 14:33:28 +0530 Subject: [PATCH 036/192] feat: patch to migrate gl entries to payment ledger --- .../v14_0/migrate_gl_to_payment_ledger.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py diff --git a/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py b/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py new file mode 100644 index 0000000000..c2267aa9af --- /dev/null +++ b/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py @@ -0,0 +1,38 @@ +import frappe +from frappe import qb + +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + get_dimensions, + make_dimension_in_accounting_doctypes, +) +from erpnext.accounts.utils import create_payment_ledger_entry + + +def create_accounting_dimension_fields(): + dimensions_and_defaults = get_dimensions() + if dimensions_and_defaults: + for dimension in dimensions_and_defaults[0]: + make_dimension_in_accounting_doctypes(dimension, ["Payment Ledger Entry"]) + + +def execute(): + # create accounting dimension fields in Payment Ledger + create_accounting_dimension_fields() + + gl = qb.DocType("GL Entry") + accounts = frappe.db.get_list( + "Account", "name", filters={"account_type": ["in", ["Receivable", "Payable"]]}, as_list=True + ) + gl_entries = [] + if accounts: + # get all gl entries on receivable/payable accounts + gl_entries = ( + qb.from_(gl) + .select("*") + .where(gl.account.isin(accounts)) + .where(gl.is_cancelled == 0) + .run(as_dict=True) + ) + if gl_entries: + # create payment ledger entries for the accounts receivable/payable + create_payment_ledger_entry(gl_entries, 0) From 7b1cb6711d16e84cea410d9dd92682a5eb72f00c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 16 May 2022 14:32:58 +0530 Subject: [PATCH 037/192] refactor: include Payment ledger in accounting dimensions list --- erpnext/hooks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 813ac17ca0..1c4bbbc3fc 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -487,6 +487,7 @@ communication_doctypes = ["Customer", "Supplier"] accounting_dimension_doctypes = [ "GL Entry", + "Payment Ledger Entry", "Sales Invoice", "Purchase Invoice", "Payment Entry", From 451cf3a937fe6f95b2d245c581ecbf38cb82cf09 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 16 May 2022 14:29:58 +0530 Subject: [PATCH 038/192] refactor: helper class for ple creation and delinking Helper functions for delinking ple and for creating payment ledger entry for transactions on receivable/payable account types --- erpnext/accounts/utils.py | 102 +++++++++++++++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 405922e16e..1869cc7b29 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -7,7 +7,7 @@ from typing import List, Tuple import frappe import frappe.defaults -from frappe import _, throw +from frappe import _, qb, throw from frappe.model.meta import get_field_precision from frappe.utils import cint, cstr, flt, formatdate, get_number_format_info, getdate, now, nowdate @@ -15,6 +15,7 @@ import erpnext # imported to enable erpnext.accounts.utils.get_account_currency from erpnext.accounts.doctype.account.account import get_account_currency # noqa +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions from erpnext.stock import get_warehouse_account_map from erpnext.stock.utils import get_stock_value_on @@ -1345,3 +1346,102 @@ def check_and_delete_linked_reports(report): if icons: for icon in icons: frappe.delete_doc("Desktop Icon", icon) + + +def create_payment_ledger_entry(gl_entries, cancel=0): + if gl_entries: + ple = None + + # companies + account = qb.DocType("Account") + companies = list(set([x.company for x in gl_entries])) + + # receivable/payable account + accounts_with_types = ( + qb.from_(account) + .select(account.name, account.account_type) + .where( + (account.account_type.isin(["Receivable", "Payable"]) & (account.company.isin(companies))) + ) + .run(as_dict=True) + ) + receivable_or_payable_accounts = [y.name for y in accounts_with_types] + + def get_account_type(account): + for entry in accounts_with_types: + if entry.name == account: + return entry.account_type + + dr_or_cr = 0 + account_type = None + for gle in gl_entries: + if gle.account in receivable_or_payable_accounts: + account_type = get_account_type(gle.account) + if account_type == "Receivable": + dr_or_cr = gle.debit - gle.credit + dr_or_cr_account_currency = gle.debit_in_account_currency - gle.credit_in_account_currency + elif account_type == "Payable": + dr_or_cr = gle.credit - gle.debit + dr_or_cr_account_currency = gle.credit_in_account_currency - gle.debit_in_account_currency + + if cancel: + dr_or_cr *= -1 + dr_or_cr_account_currency *= -1 + + ple = frappe.get_doc( + { + "doctype": "Payment Ledger Entry", + "posting_date": gle.posting_date, + "company": gle.company, + "account_type": account_type, + "account": gle.account, + "party_type": gle.party_type, + "party": gle.party, + "cost_center": gle.cost_center, + "finance_book": gle.finance_book, + "due_date": gle.due_date, + "voucher_type": gle.voucher_type, + "voucher_no": gle.voucher_no, + "against_voucher_type": gle.against_voucher_type + if gle.against_voucher_type + else gle.voucher_type, + "against_voucher_no": gle.against_voucher if gle.against_voucher else gle.voucher_no, + "currency": gle.currency, + "amount": dr_or_cr, + "amount_in_account_currency": dr_or_cr_account_currency, + "delinked": True if cancel else False, + } + ) + + dimensions_and_defaults = get_dimensions() + if dimensions_and_defaults: + for dimension in dimensions_and_defaults[0]: + ple.set(dimension.fieldname, gle.get(dimension.fieldname)) + + if cancel: + delink_original_entry(ple) + ple.flags.ignore_permissions = 1 + ple.submit() + + +def delink_original_entry(pl_entry): + if pl_entry: + ple = qb.DocType("Payment Ledger Entry") + query = ( + qb.update(ple) + .set(ple.delinked, True) + .set(ple.modified, now()) + .set(ple.modified_by, frappe.session.user) + .where( + (ple.company == pl_entry.company) + & (ple.account_type == pl_entry.account_type) + & (ple.account == pl_entry.account) + & (ple.party_type == pl_entry.party_type) + & (ple.party == pl_entry.party) + & (ple.voucher_type == pl_entry.voucher_type) + & (ple.voucher_no == pl_entry.voucher_no) + & (ple.against_voucher_type == pl_entry.against_voucher_type) + & (ple.against_voucher_no == pl_entry.against_voucher_no) + ) + ) + query.run() From 59ed7c854dae6dddba2d6b547651c6044ba01c1e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 16 May 2022 17:48:55 +0530 Subject: [PATCH 039/192] refactor: ignore linked Payment Ledger Entry in basic transactions --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 2 +- erpnext/accounts/doctype/payment_entry/payment_entry.py | 2 +- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 1 + .../accounts/doctype/purchase_invoice/purchase_invoice.py | 7 ++++++- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 7 ++++++- erpnext/buying/doctype/purchase_order/purchase_order.py | 1 + erpnext/hr/doctype/expense_claim/expense_claim.py | 2 +- erpnext/selling/doctype/sales_order/sales_order.py | 2 +- 8 files changed, 18 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index d28c3a8687..145118957b 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -94,7 +94,7 @@ class JournalEntry(AccountsController): unlink_ref_doc_from_payment_entries(self) unlink_ref_doc_from_salary_slip(self.name) - self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") self.make_gl_entries(1) self.update_advance_paid() self.update_expense_claim() diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index a3a7be2958..a10a810d1d 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -95,7 +95,7 @@ class PaymentEntry(AccountsController): self.set_status() def on_cancel(self): - self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") self.make_gl_entries(cancel=1) self.update_expense_claim() self.update_outstanding_amounts() diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 94246e135b..9649f80dee 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -96,6 +96,7 @@ class POSInvoice(SalesInvoice): ) def on_cancel(self): + self.ignore_linked_doctypes = "Payment Ledger Entry" # run on cancel method of selling controller super(SalesInvoice, self).on_cancel() if not self.is_return and self.loyalty_program: diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index a1d86e2219..e6da6669ac 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1418,7 +1418,12 @@ class PurchaseInvoice(BuyingController): frappe.db.set(self, "status", "Cancelled") unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference) - self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") + self.ignore_linked_doctypes = ( + "GL Entry", + "Stock Ledger Entry", + "Repost Item Valuation", + "Payment Ledger Entry", + ) self.update_advance_tax_references(cancel=1) def update_project(self): diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index f0880c19e3..a580d45acc 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -396,7 +396,12 @@ class SalesInvoice(SellingController): unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference) self.unlink_sales_invoice_from_timesheets() - self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") + self.ignore_linked_doctypes = ( + "GL Entry", + "Stock Ledger Entry", + "Repost Item Valuation", + "Payment Ledger Entry", + ) def update_status_updater_args(self): if cint(self.update_stock): diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 9189f18373..44426ba43d 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -323,6 +323,7 @@ class PurchaseOrder(BuyingController): update_linked_doc(self.doctype, self.name, self.inter_company_order_reference) def on_cancel(self): + self.ignore_linked_doctypes = "Payment Ledger Entry" super(PurchaseOrder, self).on_cancel() if self.is_against_so(): diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index 89d86c1bc7..589763c0a9 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -105,7 +105,7 @@ class ExpenseClaim(AccountsController): def on_cancel(self): self.update_task_and_project() - self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") if self.payable_account: self.make_gl_entries(cancel=True) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index b463213f50..7522e92a8a 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -232,7 +232,7 @@ class SalesOrder(SellingController): update_coupon_code_count(self.coupon_code, "used") def on_cancel(self): - self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") super(SalesOrder, self).on_cancel() # Cannot cancel closed SO From e88897500717de19b595b582088cd39f24221870 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 16 May 2022 14:28:25 +0530 Subject: [PATCH 040/192] refactor: link payment ledger with gl entry creation --- erpnext/accounts/general_ledger.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 1598d914e2..b0513f16a5 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -14,6 +14,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, ) from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget +from erpnext.accounts.utils import create_payment_ledger_entry class ClosedAccountingPeriod(frappe.ValidationError): @@ -34,6 +35,7 @@ def make_gl_entries( validate_disabled_accounts(gl_map) gl_map = process_gl_map(gl_map, merge_entries) if gl_map and len(gl_map) > 1: + create_payment_ledger_entry(gl_map) save_entries(gl_map, adv_adj, update_outstanding, from_repost) # Post GL Map proccess there may no be any GL Entries elif gl_map: @@ -479,6 +481,7 @@ def make_reverse_gl_entries( ).run(as_dict=1) if gl_entries: + create_payment_ledger_entry(gl_entries, cancel=1) validate_accounting_period(gl_entries) check_freezing_date(gl_entries[0]["posting_date"], adv_adj) set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"]) From dac678e3fc475e45afaea43ecbcb0584a3074555 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 16 May 2022 14:52:50 +0530 Subject: [PATCH 041/192] test: payment ledger entry --- .../test_payment_ledger_entry.py | 217 +++++++++++++++++- 1 file changed, 205 insertions(+), 12 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 f874b75432..a71b19e092 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 @@ -1,30 +1,27 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest - import frappe from frappe import qb from frappe.tests.utils import FrappeTestCase -from frappe.utils import add_days, nowdate +from frappe.utils import nowdate from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice -from erpnext.accounts.party import get_party_account from erpnext.stock.doctype.item.test_item import create_item -# class TestPaymentLedgerEntry(FrappeTestCase): -class TestPaymentLedgerEntry(unittest.TestCase): +class TestPaymentLedgerEntry(FrappeTestCase): def setUp(self): + self.ple = qb.DocType("Payment Ledger Entry") self.create_company() self.create_item() self.create_customer() self.clear_old_entries() - # def tearDown(self): - # frappe.db.rollback() + def tearDown(self): + frappe.db.rollback() def create_company(self): company_name = "_Test Payment Ledger" @@ -170,12 +167,55 @@ class TestPaymentLedgerEntry(unittest.TestCase): ) return je - def test_create_all_types(self): + def test_payment_against_invoice(self): transaction_date = nowdate() amount = 100 + ple = self.ple + # full payment using PE si1 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date) - pe2 = get_payment_entry(si1.doctype, si1.name).save().submit() + pe1 = get_payment_entry(si1.doctype, si1.name).save().submit() + + pl_entries = ( + 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 == si1.doctype) & (ple.against_voucher_no == si1.name)) + .orderby(ple.creation) + .run(as_dict=True) + ) + + expected_values = [ + { + "voucher_type": si1.doctype, + "voucher_no": si1.name, + "against_voucher_type": si1.doctype, + "against_voucher_no": si1.name, + "amount": amount, + "delinked": 0, + }, + { + "voucher_type": pe1.doctype, + "voucher_no": pe1.name, + "against_voucher_type": si1.doctype, + "against_voucher_no": si1.name, + "amount": -amount, + "delinked": 0, + }, + ] + self.assertEqual(pl_entries[0], expected_values[0]) + self.assertEqual(pl_entries[1], expected_values[1]) + + def test_partial_payment_against_invoice(self): + ple = self.ple + transaction_date = nowdate() + amount = 100 # partial payment of invoice using PE si2 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date) @@ -184,6 +224,47 @@ class TestPaymentLedgerEntry(unittest.TestCase): pe2.get("references")[0].outstanding_amount = 50 pe2 = pe2.save().submit() + pl_entries = ( + 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 == si2.doctype) & (ple.against_voucher_no == si2.name)) + .orderby(ple.creation) + .run(as_dict=True) + ) + + expected_values = [ + { + "voucher_type": si2.doctype, + "voucher_no": si2.name, + "against_voucher_type": si2.doctype, + "against_voucher_no": si2.name, + "amount": amount, + "delinked": 0, + }, + { + "voucher_type": pe2.doctype, + "voucher_no": pe2.name, + "against_voucher_type": si2.doctype, + "against_voucher_no": si2.name, + "amount": -50, + "delinked": 0, + }, + ] + self.assertEqual(pl_entries[0], expected_values[0]) + self.assertEqual(pl_entries[1], expected_values[1]) + + def test_cr_note_against_invoice(self): + ple = self.ple + transaction_date = nowdate() + amount = 100 + # reconcile against return invoice si3 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date) cr_note1 = self.create_sales_invoice( @@ -193,6 +274,47 @@ class TestPaymentLedgerEntry(unittest.TestCase): cr_note1.return_against = si3.name cr_note1 = cr_note1.save().submit() + pl_entries = ( + 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 == si3.doctype) & (ple.against_voucher_no == si3.name)) + .orderby(ple.creation) + .run(as_dict=True) + ) + + expected_values = [ + { + "voucher_type": si3.doctype, + "voucher_no": si3.name, + "against_voucher_type": si3.doctype, + "against_voucher_no": si3.name, + "amount": amount, + "delinked": 0, + }, + { + "voucher_type": cr_note1.doctype, + "voucher_no": cr_note1.name, + "against_voucher_type": si3.doctype, + "against_voucher_no": si3.name, + "amount": -amount, + "delinked": 0, + }, + ] + self.assertEqual(pl_entries[0], expected_values[0]) + self.assertEqual(pl_entries[1], expected_values[1]) + + def test_je_against_inv_and_note(self): + ple = self.ple + transaction_date = nowdate() + amount = 100 + # reconcile against return invoice using JE si4 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date) cr_note2 = self.create_sales_invoice( @@ -211,5 +333,76 @@ class TestPaymentLedgerEntry(unittest.TestCase): je1.get("accounts")[1].reference_name = si4.name je1 = je1.save().submit() - def test_dummy(self): - pass + pl_entries_for_invoice = ( + 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 == si4.doctype) & (ple.against_voucher_no == si4.name)) + .orderby(ple.creation) + .run(as_dict=True) + ) + + expected_values = [ + { + "voucher_type": si4.doctype, + "voucher_no": si4.name, + "against_voucher_type": si4.doctype, + "against_voucher_no": si4.name, + "amount": amount, + "delinked": 0, + }, + { + "voucher_type": je1.doctype, + "voucher_no": je1.name, + "against_voucher_type": si4.doctype, + "against_voucher_no": si4.name, + "amount": -amount, + "delinked": 0, + }, + ] + self.assertEqual(pl_entries_for_invoice[0], expected_values[0]) + self.assertEqual(pl_entries_for_invoice[1], expected_values[1]) + + pl_entries_for_crnote = ( + 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_note2.doctype) & (ple.against_voucher_no == cr_note2.name) + ) + .orderby(ple.creation) + .run(as_dict=True) + ) + + expected_values = [ + { + "voucher_type": cr_note2.doctype, + "voucher_no": cr_note2.name, + "against_voucher_type": cr_note2.doctype, + "against_voucher_no": cr_note2.name, + "amount": -amount, + "delinked": 0, + }, + { + "voucher_type": je1.doctype, + "voucher_no": je1.name, + "against_voucher_type": cr_note2.doctype, + "against_voucher_no": cr_note2.name, + "amount": amount, + "delinked": 0, + }, + ] + self.assertEqual(pl_entries_for_crnote[0], expected_values[0]) + self.assertEqual(pl_entries_for_crnote[1], expected_values[1]) From e625394488626e35f5f18af843554fe240e1f0d2 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 21 May 2022 12:40:59 +0530 Subject: [PATCH 042/192] test: simplify job card tests --- .../doctype/job_card/test_job_card.py | 103 ++++++++---------- 1 file changed, 47 insertions(+), 56 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 4647ddf05f..45d221f0f7 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -1,6 +1,9 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt + +from typing import Literal + import frappe from frappe.tests.utils import FrappeTestCase from frappe.utils import random_string @@ -17,34 +20,36 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry class TestJobCard(FrappeTestCase): def setUp(self): make_bom_for_jc_tests() + self.transfer_material_against: Literal["Work Order", "Job Card"] = "Work Order" + self.source_warehouse = None + self._work_order = None - transfer_material_against, source_warehouse = None, None + @property + def work_order(self): + """Work Order lazily created for tests.""" + if not self._work_order: + self._work_order = make_wo_order_test_record( + item="_Test FG Item 2", + qty=2, + transfer_material_against=self.transfer_material_against, + source_warehouse=self.source_warehouse, + ) + return self._work_order - tests_that_skip_setup = ("test_job_card_material_transfer_correctness",) - tests_that_transfer_against_jc = ( - "test_job_card_multiple_materials_transfer", - "test_job_card_excess_material_transfer", - "test_job_card_partial_material_transfer", - ) - - if self._testMethodName in tests_that_skip_setup: - return - - if self._testMethodName in tests_that_transfer_against_jc: - transfer_material_against = "Job Card" - source_warehouse = "Stores - _TC" - - self.work_order = make_wo_order_test_record( - item="_Test FG Item 2", - qty=2, - transfer_material_against=transfer_material_against, - source_warehouse=source_warehouse, - ) + def generate_required_stock(self, work_order: WorkOrder) -> None: + """Create twice the stock for all required items in work order.""" + for item in work_order.required_items: + make_stock_entry( + item_code=item.item_code, + target=item.source_warehouse or self.source_warehouse, + qty=item.required_qty * 2, + basic_rate=100, + ) def tearDown(self): frappe.db.rollback() - def test_job_card(self): + def test_job_card_operations(self): job_cards = frappe.get_all( "Job Card", filters={"work_order": self.work_order.name}, fields=["operation_id", "name"] @@ -58,9 +63,6 @@ class TestJobCard(FrappeTestCase): doc.operation_id = "Test Data" self.assertRaises(OperationMismatchError, doc.save) - for d in job_cards: - frappe.delete_doc("Job Card", d.name) - def test_job_card_with_different_work_station(self): job_cards = frappe.get_all( "Job Card", @@ -96,19 +98,11 @@ class TestJobCard(FrappeTestCase): ) self.assertEqual(completed_qty, job_card.for_quantity) - doc.cancel() - - for d in job_cards: - frappe.delete_doc("Job Card", d.name) - def test_job_card_overlap(self): wo2 = make_wo_order_test_record(item="_Test FG Item 2", qty=2) - jc1_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name}) - jc2_name = frappe.db.get_value("Job Card", {"work_order": wo2.name}) - - jc1 = frappe.get_doc("Job Card", jc1_name) - jc2 = frappe.get_doc("Job Card", jc2_name) + jc1 = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name}) + jc2 = frappe.get_last_doc("Job Card", {"work_order": wo2.name}) employee = "_T-Employee-00001" # from test records @@ -137,10 +131,10 @@ class TestJobCard(FrappeTestCase): def test_job_card_multiple_materials_transfer(self): "Test transferring RMs separately against Job Card with multiple RMs." - make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=100) - make_stock_entry( - item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=6, basic_rate=100 - ) + self.transfer_material_against = "Job Card" + self.source_warehouse = "Stores - _TC" + + self.generate_required_stock(self.work_order) job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name}) job_card = frappe.get_doc("Job Card", job_card_name) @@ -167,22 +161,21 @@ class TestJobCard(FrappeTestCase): def test_job_card_excess_material_transfer(self): "Test transferring more than required RM against Job Card." - make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=25, basic_rate=100) - make_stock_entry( - item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=15, basic_rate=100 - ) + self.transfer_material_against = "Job Card" + self.source_warehouse = "Stores - _TC" - job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name}) - job_card = frappe.get_doc("Job Card", job_card_name) + self.generate_required_stock(self.work_order) + + job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name}) self.assertEqual(job_card.status, "Open") # fully transfer both RMs - transfer_entry_1 = make_stock_entry_from_jc(job_card_name) + transfer_entry_1 = make_stock_entry_from_jc(job_card.name) transfer_entry_1.insert() transfer_entry_1.submit() # transfer extra qty of both RM due to previously damaged RM - transfer_entry_2 = make_stock_entry_from_jc(job_card_name) + transfer_entry_2 = make_stock_entry_from_jc(job_card.name) # deliberately change 'For Quantity' transfer_entry_2.fg_completed_qty = 1 transfer_entry_2.items[0].qty = 5 @@ -195,7 +188,7 @@ class TestJobCard(FrappeTestCase): # Check if 'For Quantity' is negative # as 'transferred_qty' > Qty to Manufacture - transfer_entry_3 = make_stock_entry_from_jc(job_card_name) + transfer_entry_3 = make_stock_entry_from_jc(job_card.name) self.assertEqual(transfer_entry_3.fg_completed_qty, 0) job_card.append( @@ -210,17 +203,15 @@ class TestJobCard(FrappeTestCase): def test_job_card_partial_material_transfer(self): "Test partial material transfer against Job Card" + self.transfer_material_against = "Job Card" + self.source_warehouse = "Stores - _TC" - make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=25, basic_rate=100) - make_stock_entry( - item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=15, basic_rate=100 - ) + self.generate_required_stock(self.work_order) - job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name}) - job_card = frappe.get_doc("Job Card", job_card_name) + job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name}) # partially transfer - transfer_entry = make_stock_entry_from_jc(job_card_name) + transfer_entry = make_stock_entry_from_jc(job_card.name) transfer_entry.fg_completed_qty = 1 transfer_entry.get_items() transfer_entry.insert() @@ -232,7 +223,7 @@ class TestJobCard(FrappeTestCase): self.assertEqual(transfer_entry.items[1].qty, 3) # transfer remaining - transfer_entry_2 = make_stock_entry_from_jc(job_card_name) + transfer_entry_2 = make_stock_entry_from_jc(job_card.name) self.assertEqual(transfer_entry_2.fg_completed_qty, 1) self.assertEqual(transfer_entry_2.items[0].qty, 5) From 66cf9aa3441d709c910204be1bedeea1437d7620 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 21 May 2022 12:09:47 +0530 Subject: [PATCH 043/192] fix: creation of corrective job card fails This used to fail because sub_operations is a child table that's not initalized by default till v13, in develop branch we init tables with empty list. --- .../doctype/job_card/job_card.py | 1 + .../doctype/job_card/test_job_card.py | 56 +++++++++++++++++-- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index a98fc94868..b13e4e0c04 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -866,6 +866,7 @@ def make_corrective_job_card(source_name, operation=None, for_operation=None, ta target.set("time_logs", []) target.set("employee", []) target.set("items", []) + target.set("sub_operations", []) target.set_sub_operations() target.get_required_items() target.validate_time_logs() diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 45d221f0f7..25a03eaf03 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -5,14 +5,20 @@ from typing import Literal import frappe -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import random_string +from frappe.utils.data import add_to_date, now -from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError, OverlapError +from erpnext.manufacturing.doctype.job_card.job_card import ( + OperationMismatchError, + OverlapError, + make_corrective_job_card, +) from erpnext.manufacturing.doctype.job_card.job_card import ( make_stock_entry as make_stock_entry_from_jc, ) from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record +from erpnext.manufacturing.doctype.work_order.work_order import WorkOrder from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry @@ -25,7 +31,7 @@ class TestJobCard(FrappeTestCase): self._work_order = None @property - def work_order(self): + def work_order(self) -> WorkOrder: """Work Order lazily created for tests.""" if not self._work_order: self._work_order = make_wo_order_test_record( @@ -268,7 +274,49 @@ class TestJobCard(FrappeTestCase): self.assertEqual(transfer_entry.items[0].item_code, "_Test Item") self.assertEqual(transfer_entry.items[0].qty, 2) - # rollback via tearDown method + @change_settings( + "Manufacturing Settings", {"add_corrective_operation_cost_in_finished_good_valuation": 1} + ) + def test_corrective_costing(self): + job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name}) + + job_card.append( + "time_logs", + {"from_time": now(), "to_time": add_to_date(now(), hours=1), "completed_qty": 2}, + ) + job_card.submit() + + self.work_order.reload() + original_cost = self.work_order.total_operating_cost + + # Create a corrective operation against it + corrective_action = frappe.get_doc( + doctype="Operation", is_corrective_operation=1, name=frappe.generate_hash() + ).insert() + + corrective_job_card = make_corrective_job_card( + job_card.name, operation=corrective_action.name, for_operation=job_card.operation + ) + corrective_job_card.hour_rate = 100 + corrective_job_card.insert() + corrective_job_card.append( + "time_logs", + { + "from_time": add_to_date(now(), hours=2), + "to_time": add_to_date(now(), hours=2, minutes=30), + "completed_qty": 2, + }, + ) + corrective_job_card.submit() + + self.work_order.reload() + cost_after_correction = self.work_order.total_operating_cost + self.assertGreater(cost_after_correction, original_cost) + + corrective_job_card.cancel() + self.work_order.reload() + cost_after_cancel = self.work_order.total_operating_cost + self.assertEqual(cost_after_cancel, original_cost) def create_bom_with_multiple_operations(): From a29b92febc4397cebb251d4d3f34210e4fb85c21 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 23 May 2022 10:08:20 +0530 Subject: [PATCH 044/192] fix: Use directly and style it as button instead of using button Since few email servers (like outlook) strips out link in the button making them unclickable. --- .../emails/request_for_quotation.html | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/erpnext/templates/emails/request_for_quotation.html b/erpnext/templates/emails/request_for_quotation.html index 3283987fab..5b073e604f 100644 --- a/erpnext/templates/emails/request_for_quotation.html +++ b/erpnext/templates/emails/request_for_quotation.html @@ -1,24 +1,29 @@

{{_("Request for Quotation")}}

{{ supplier_salutation if supplier_salutation else ''}} {{ supplier_name }},

{{ message }}

-

{{_("The Request for Quotation can be accessed by clicking on the following button")}}:

-

- -


- -

{{_("Regards")}},
-{{ user_fullname }}


- +
+ + {{ _("Submit your Quotation") }} + +
+
{% if update_password_link %} - +

{{_("Please click on the following button to set your new password")}}:

-

- -

- + + {{_("Set Password") }} + +
+
{% endif %} +

+ {{_("Regards")}},
+ {{ user_fullname }} +

From 34e238ce4f1dcd32de72851e44613c0b67018db7 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 23 May 2022 10:13:49 +0530 Subject: [PATCH 045/192] fix: Leave Encashment calculations (#31062) --- .../leave_encashment/leave_encashment.py | 34 ++++--- .../leave_encashment/test_leave_encashment.py | 89 +++++++++++++------ 2 files changed, 86 insertions(+), 37 deletions(-) diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.py b/erpnext/hr/doctype/leave_encashment/leave_encashment.py index 0f655e3e0f..7c0f0db197 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.py @@ -7,7 +7,7 @@ from frappe import _ from frappe.model.document import Document from frappe.utils import getdate, nowdate -from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leaves +from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry from erpnext.hr.utils import set_employee_name, validate_active_employee from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import ( @@ -107,7 +107,10 @@ class LeaveEncashment(Document): self.leave_balance = ( allocation.total_leaves_allocated - allocation.carry_forwarded_leaves_count - - get_unused_leaves(self.employee, self.leave_type, allocation.from_date, self.encashment_date) + # adding this because the function returns a -ve number + + get_leaves_for_period( + self.employee, self.leave_type, allocation.from_date, self.encashment_date + ) ) encashable_days = self.leave_balance - frappe.db.get_value( @@ -126,14 +129,25 @@ class LeaveEncashment(Document): return True def get_leave_allocation(self): - leave_allocation = frappe.db.sql( - """select name, to_date, total_leaves_allocated, carry_forwarded_leaves_count from `tabLeave Allocation` where '{0}' - between from_date and to_date and docstatus=1 and leave_type='{1}' - and employee= '{2}'""".format( - self.encashment_date or getdate(nowdate()), self.leave_type, self.employee - ), - as_dict=1, - ) # nosec + date = self.encashment_date or getdate() + + LeaveAllocation = frappe.qb.DocType("Leave Allocation") + leave_allocation = ( + frappe.qb.from_(LeaveAllocation) + .select( + LeaveAllocation.name, + LeaveAllocation.from_date, + LeaveAllocation.to_date, + LeaveAllocation.total_leaves_allocated, + LeaveAllocation.carry_forwarded_leaves_count, + ) + .where( + ((LeaveAllocation.from_date <= date) & (date <= LeaveAllocation.to_date)) + & (LeaveAllocation.docstatus == 1) + & (LeaveAllocation.leave_type == self.leave_type) + & (LeaveAllocation.employee == self.employee) + ) + ).run(as_dict=True) return leave_allocation[0] if leave_allocation else None diff --git a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py index 83eb969feb..d06b6a3764 100644 --- a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py @@ -4,26 +4,42 @@ import unittest import frappe -from frappe.utils import add_months, today +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days, get_year_ending, get_year_start, getdate from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_period from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_policy from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import ( create_assignment_for_multiple_employees, ) +from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( + make_holiday_list, + make_leave_application, +) from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure -test_dependencies = ["Leave Type"] +test_records = frappe.get_test_records("Leave Type") -class TestLeaveEncashment(unittest.TestCase): +class TestLeaveEncashment(FrappeTestCase): def setUp(self): - frappe.db.sql("""delete from `tabLeave Period`""") - frappe.db.sql("""delete from `tabLeave Policy Assignment`""") - frappe.db.sql("""delete from `tabLeave Allocation`""") - frappe.db.sql("""delete from `tabLeave Ledger Entry`""") - frappe.db.sql("""delete from `tabAdditional Salary`""") + frappe.db.delete("Leave Period") + frappe.db.delete("Leave Policy Assignment") + frappe.db.delete("Leave Allocation") + frappe.db.delete("Leave Ledger Entry") + frappe.db.delete("Additional Salary") + frappe.db.delete("Leave Encashment") + + if not frappe.db.exists("Leave Type", "_Test Leave Type Encashment"): + frappe.get_doc(test_records[2]).insert() + + date = getdate() + year_start = getdate(get_year_start(date)) + year_end = getdate(get_year_ending(date)) + + make_holiday_list("_Test Leave Encashment", year_start, year_end) # create the leave policy leave_policy = create_leave_policy( @@ -32,9 +48,9 @@ class TestLeaveEncashment(unittest.TestCase): leave_policy.submit() # create employee, salary structure and assignment - self.employee = make_employee("test_employee_encashment@example.com") + self.employee = make_employee("test_employee_encashment@example.com", company="_Test Company") - self.leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3)) + self.leave_period = create_leave_period(year_start, year_end, "_Test Company") data = { "assignment_based_on": "Leave Period", @@ -53,27 +69,15 @@ class TestLeaveEncashment(unittest.TestCase): other_details={"leave_encashment_amount_per_day": 50}, ) - def tearDown(self): - for dt in [ - "Leave Period", - "Leave Allocation", - "Leave Ledger Entry", - "Additional Salary", - "Leave Encashment", - "Salary Structure", - "Leave Policy", - ]: - frappe.db.sql("delete from `tab%s`" % dt) - + @set_holiday_list("_Test Leave Encashment", "_Test Company") def test_leave_balance_value_and_amount(self): - frappe.db.sql("""delete from `tabLeave Encashment`""") leave_encashment = frappe.get_doc( dict( doctype="Leave Encashment", employee=self.employee, leave_type="_Test Leave Type Encashment", leave_period=self.leave_period.name, - payroll_date=today(), + encashment_date=self.leave_period.to_date, currency="INR", ) ).insert() @@ -88,15 +92,46 @@ class TestLeaveEncashment(unittest.TestCase): add_sal = frappe.get_all("Additional Salary", filters={"ref_docname": leave_encashment.name})[0] self.assertTrue(add_sal) - def test_creation_of_leave_ledger_entry_on_submit(self): - frappe.db.sql("""delete from `tabLeave Encashment`""") + @set_holiday_list("_Test Leave Encashment", "_Test Company") + def test_leave_balance_value_with_leaves_and_amount(self): + date = self.leave_period.from_date + leave_application = make_leave_application( + self.employee, date, add_days(date, 3), "_Test Leave Type Encashment" + ) + leave_application.reload() + leave_encashment = frappe.get_doc( dict( doctype="Leave Encashment", employee=self.employee, leave_type="_Test Leave Type Encashment", leave_period=self.leave_period.name, - payroll_date=today(), + encashment_date=self.leave_period.to_date, + currency="INR", + ) + ).insert() + + self.assertEqual(leave_encashment.leave_balance, 10 - leave_application.total_leave_days) + # encashable days threshold is 5, total leaves are 6, so encashable days = 6-5 = 1 + # with charge of 50 per day + self.assertEqual(leave_encashment.encashable_days, leave_encashment.leave_balance - 5) + self.assertEqual(leave_encashment.encashment_amount, 50) + + leave_encashment.submit() + + # assert links + add_sal = frappe.get_all("Additional Salary", filters={"ref_docname": leave_encashment.name})[0] + self.assertTrue(add_sal) + + @set_holiday_list("_Test Leave Encashment", "_Test Company") + def test_creation_of_leave_ledger_entry_on_submit(self): + leave_encashment = frappe.get_doc( + dict( + doctype="Leave Encashment", + employee=self.employee, + leave_type="_Test Leave Type Encashment", + leave_period=self.leave_period.name, + encashment_date=self.leave_period.to_date, currency="INR", ) ).insert() From 9f6e10663b77489ba1f98ede96e30c23682c111a Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 23 May 2022 11:05:55 +0530 Subject: [PATCH 046/192] chore: Run `_validate_over_transfer` only if excess transfer is blocked in settings --- erpnext/manufacturing/doctype/job_card/job_card.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index d16281a5ba..0a9fd8a099 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -531,11 +531,9 @@ class JobCard(Document): def _validate_over_transfer(row, transferred_qty): "Block over transfer of items if not allowed in settings." - allow_excess = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer") required_qty = frappe.db.get_value("Job Card Item", row.job_card_item, "required_qty") is_excess = flt(transferred_qty) > flt(required_qty) - - if is_excess and not allow_excess: + if is_excess: frappe.throw( _( "Row #{0}: Cannot transfer more than Required Qty {1} for Item {2} against Job Card {3}" @@ -564,7 +562,9 @@ class JobCard(Document): ) ).run()[0][0] - _validate_over_transfer(row, transferred_qty) + allow_excess = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer") + if not allow_excess: + _validate_over_transfer(row, transferred_qty) frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty)) From 348a674df968dd9fe754fc8e85eef9cb51c227f4 Mon Sep 17 00:00:00 2001 From: Wolfram Schmidt Date: Mon, 23 May 2022 07:53:12 +0200 Subject: [PATCH 047/192] fix translation German "Designation" (#31082) changed "Bezeichnung" to "Position" as the is more precice in the field of employment which erpnext refers to here --- erpnext/translations/de.csv | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index ccd613dc37..8730c4ecd3 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -1701,7 +1701,7 @@ No Permission,Keine Berechtigung, No Remarks,Keine Anmerkungen, No Result to submit,Kein Ergebnis zur Einreichung, No Salary Structure assigned for Employee {0} on given date {1},Keine Gehaltsstruktur für Mitarbeiter {0} am angegebenen Datum {1} zugewiesen, -No Staffing Plans found for this Designation,Für diese Bezeichnung wurden keine Stellenpläne gefunden, +No Staffing Plans found for this Designation,Für diese Position wurden keine Stellenpläne gefunden, No Student Groups created.,Keine Studentengruppen erstellt., No Students in,Keine Studenten in, No Tax Withholding data found for the current Fiscal Year.,Keine Steuerverweigerungsdaten für das aktuelle Geschäftsjahr gefunden., @@ -2027,7 +2027,7 @@ Please select BOM in BOM field for Item {0},Bitte aus dem Stücklistenfeld eine Please select Category first,Bitte zuerst Kategorie auswählen, Please select Charge Type first,Bitte zuerst Chargentyp auswählen, Please select Company,Bitte Unternehmen auswählen, -Please select Company and Designation,Bitte wählen Sie Unternehmen und Stelle, +Please select Company and Designation,Bitte wählen Sie Unternehmen und Position, Please select Company and Posting Date to getting entries,"Bitte wählen Sie Unternehmen und Buchungsdatum, um Einträge zu erhalten", Please select Company first,Bitte zuerst Unternehmen auswählen, Please select Completion Date for Completed Asset Maintenance Log,Bitte wählen Sie Fertigstellungsdatum für das abgeschlossene Wartungsprotokoll für den Vermögenswert, @@ -2772,7 +2772,7 @@ Split,Teilt, Split Batch,Split Batch, Split Issue,Split-Problem, Sports,Sport, -Staffing Plan {0} already exist for designation {1},Personalplan {0} existiert bereits für Bezeichnung {1}, +Staffing Plan {0} already exist for designation {1},Personalplan {0} existiert bereits für Position {1}, Standard,Standard, Standard Buying,Standard-Kauf, Standard Selling,Standard-Vertrieb, @@ -3710,7 +3710,7 @@ Delivered Quantity,Gelieferte Menge, Delivery Notes,Lieferscheine, Depreciated Amount,Abschreibungsbetrag, Description,Beschreibung, -Designation,Bezeichnung, +Designation,Position, Difference Value,Differenzwert, Dimension Filter,Dimensionsfilter, Disabled,Deaktiviert, @@ -3920,7 +3920,7 @@ Please enter Difference Account or set default Stock Adjustment Accoun Please enter GSTIN and state for the Company Address {0},Bitte geben Sie GSTIN ein und geben Sie die Firmenadresse {0} an., Please enter Item Code to get item taxes,"Bitte geben Sie den Artikelcode ein, um die Artikelsteuern zu erhalten", Please enter Warehouse and Date,Bitte geben Sie Lager und Datum ein, -Please enter the designation,Bitte geben Sie die Bezeichnung ein, +Please enter the designation,Bitte geben Sie die Position ein, Please login as a Marketplace User to edit this item.,"Bitte melden Sie sich als Marketplace-Benutzer an, um diesen Artikel zu bearbeiten.", Please login as a Marketplace User to report this item.,"Bitte melden Sie sich als Marketplace-Benutzer an, um diesen Artikel zu melden.", Please select Template Type to download template,"Bitte wählen Sie Vorlagentyp , um die Vorlage herunterzuladen", @@ -6243,7 +6243,7 @@ Checking this will create Lab Test(s) specified in the Sales Invoice on submissi Create Sample Collection document for Lab Test,Erstellen Sie ein Probensammeldokument für den Labortest, Checking this will create a Sample Collection document every time you create a Lab Test,"Wenn Sie dies aktivieren, wird jedes Mal, wenn Sie einen Labortest erstellen, ein Probensammeldokument erstellt", Employee name and designation in print,Name und Bezeichnung des Mitarbeiters im Druck, -Check this if you want the Name and Designation of the Employee associated with the User who submits the document to be printed in the Lab Test Report.,"Aktivieren Sie diese Option, wenn Sie möchten, dass der Name und die Bezeichnung des Mitarbeiters, der dem Benutzer zugeordnet ist, der das Dokument einreicht, im Labortestbericht gedruckt werden.", +Check this if you want the Name and Designation of the Employee associated with the User who submits the document to be printed in the Lab Test Report.,"Aktivieren Sie diese Option, wenn Sie möchten, dass der Name und die Position des Mitarbeiters, der dem Benutzer zugeordnet ist, der das Dokument einreicht, im Labortestbericht gedruckt werden.", Do not print or email Lab Tests without Approval,Drucken oder senden Sie Labortests nicht ohne Genehmigung per E-Mail, Checking this will restrict printing and emailing of Lab Test documents unless they have the status as Approved.,"Wenn Sie dies aktivieren, wird das Drucken und E-Mailen von Labortestdokumenten eingeschränkt, sofern diese nicht den Status "Genehmigt" haben.", Custom Signature in Print,Kundenspezifische Unterschrift im Druck, @@ -6499,7 +6499,7 @@ Department Approver,Abteilungsgenehmiger, Approver,Genehmiger, Required Skills,Benötigte Fähigkeiten, Skills,Kompetenzen, -Designation Skill,Bezeichnung Fähigkeit, +Designation Skill,Positions Fähigkeit, Skill,Fertigkeit, Driver,Fahrer/-in, HR-DRI-.YYYY.-,HR-DRI-.YYYY.-, @@ -6798,7 +6798,7 @@ Select Employees,Mitarbeiter auswählen, Employment Type (optional),Anstellungsart (optional), Branch (optional),Zweigstelle (optional), Department (optional),Abteilung (optional), -Designation (optional),Bezeichnung (optional), +Designation (optional),Position (optional), Employee Grade (optional),Dienstgrad (optional), Employee (optional),Mitarbeiter (optional), Allocate Leaves,Blätter zuweisen, @@ -7769,7 +7769,7 @@ Authorized Value,Autorisierter Wert, Applicable To (Role),Anwenden auf (Rolle), Applicable To (Employee),Anwenden auf (Mitarbeiter), Applicable To (User),Anwenden auf (Benutzer), -Applicable To (Designation),Anwenden auf (Bezeichnung), +Applicable To (Designation),Anwenden auf (Position), Approving Role (above authorized value),Genehmigende Rolle (über dem autorisierten Wert), Approving User (above authorized value),Genehmigender Benutzer (über dem autorisierten Wert), Brand Defaults,Markenstandards, @@ -8946,7 +8946,7 @@ Requesting Practitioner,Praktizierender anfordern, Requesting Department,Abteilung anfordern, Employee (Lab Technician),Mitarbeiter (Labortechniker), Lab Technician Name,Name des Labortechnikers, -Lab Technician Designation,Bezeichnung des Labortechnikers, +Lab Technician Designation,Position des Labortechnikers, Compound Test Result,Zusammengesetztes Testergebnis, Organism Test Result,Organismustestergebnis, Sensitivity Test Result,Empfindlichkeitstestergebnis, From a36174afdf09e70e636aca2125a6cb092fb24735 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 23 May 2022 15:12:11 +0530 Subject: [PATCH 048/192] test: search test failing because of stale data (#31098) --- erpnext/tests/test_search.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/tests/test_search.py b/erpnext/tests/test_search.py index ffe9a5ae54..3685828667 100644 --- a/erpnext/tests/test_search.py +++ b/erpnext/tests/test_search.py @@ -8,6 +8,7 @@ class TestSearch(unittest.TestCase): # Search for the word "cond", part of the word "conduire" (Lead) in french. def test_contact_search_in_foreign_language(self): try: + frappe.local.lang_full_dict = None # reset cached translations frappe.local.lang = "fr" output = filter_dynamic_link_doctypes( "DocType", "cond", "name", 0, 20, {"fieldtype": "HTML", "fieldname": "contact_html"} From c704ad889d1c86b1fc3d94e27ff55851452b0c29 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 23 May 2022 15:18:24 +0200 Subject: [PATCH 049/192] style: format warehouse js --- erpnext/stock/doctype/warehouse/warehouse.js | 111 +++++++++++-------- 1 file changed, 64 insertions(+), 47 deletions(-) diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js index 9243e1ed84..6baaf378fa 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.js +++ b/erpnext/stock/doctype/warehouse/warehouse.js @@ -1,88 +1,105 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt - frappe.ui.form.on("Warehouse", { - onload: function(frm) { - frm.set_query("default_in_transit_warehouse", function() { + onload: function (frm) { + frm.set_query("default_in_transit_warehouse", function () { return { - filters:{ - 'warehouse_type' : 'Transit', - 'is_group': 0, - 'company': frm.doc.company - } + filters: { + warehouse_type: "Transit", + is_group: 0, + company: frm.doc.company, + }, }; }); }, - refresh: function(frm) { - frm.toggle_display('warehouse_name', frm.doc.__islocal); - frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal); + refresh: function (frm) { + frm.toggle_display("warehouse_name", frm.doc.__islocal); + frm.toggle_display( + ["address_html", "contact_html"], + !frm.doc.__islocal + ); - - if(!frm.doc.__islocal) { + if (!frm.doc.__islocal) { frappe.contacts.render_address_and_contact(frm); - } else { frappe.contacts.clear_address_and_contact(frm); } - frm.add_custom_button(__("Stock Balance"), function() { - frappe.set_route("query-report", "Stock Balance", {"warehouse": frm.doc.name}); + frm.add_custom_button(__("Stock Balance"), function () { + frappe.set_route("query-report", "Stock Balance", { + warehouse: frm.doc.name, + }); }); if (cint(frm.doc.is_group) == 1) { - frm.add_custom_button(__('Group to Non-Group'), - function() { convert_to_group_or_ledger(frm); }, 'fa fa-retweet', 'btn-default') + frm.add_custom_button( + __("Group to Non-Group"), + function () { + convert_to_group_or_ledger(frm); + }, + "fa fa-retweet", + "btn-default" + ); } else if (cint(frm.doc.is_group) == 0) { - if(frm.doc.__onload && frm.doc.__onload.account) { - frm.add_custom_button(__("General Ledger"), function() { + if (frm.doc.__onload && frm.doc.__onload.account) { + frm.add_custom_button(__("General Ledger"), function () { frappe.route_options = { - "account": frm.doc.__onload.account, - "company": frm.doc.company - } + account: frm.doc.__onload.account, + company: frm.doc.company, + }; frappe.set_route("query-report", "General Ledger"); }); } - frm.add_custom_button(__('Non-Group to Group'), - function() { convert_to_group_or_ledger(frm); }, 'fa fa-retweet', 'btn-default') + frm.add_custom_button( + __("Non-Group to Group"), + function () { + convert_to_group_or_ledger(frm); + }, + "fa fa-retweet", + "btn-default" + ); } - frm.toggle_enable(['is_group', 'company'], false); + frm.toggle_enable(["is_group", "company"], false); - frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Warehouse'}; + frappe.dynamic_link = { + doc: frm.doc, + fieldname: "name", + doctype: "Warehouse", + }; - frm.fields_dict['parent_warehouse'].get_query = function(doc) { + frm.fields_dict["parent_warehouse"].get_query = function (doc) { return { filters: { - "is_group": 1, - } - } - } + is_group: 1, + }, + }; + }; - frm.fields_dict['account'].get_query = function(doc) { + frm.fields_dict["account"].get_query = function (doc) { return { filters: { - "is_group": 0, - "account_type": "Stock", - "company": frm.doc.company - } - } - } - } + is_group: 0, + account_type: "Stock", + company: frm.doc.company, + }, + }; + }; + }, }); -function convert_to_group_or_ledger(frm){ +function convert_to_group_or_ledger(frm) { frappe.call({ - method:"erpnext.stock.doctype.warehouse.warehouse.convert_to_group_or_ledger", + method: "erpnext.stock.doctype.warehouse.warehouse.convert_to_group_or_ledger", args: { docname: frm.doc.name, - is_group: frm.doc.is_group + is_group: frm.doc.is_group, }, - callback: function(){ + callback: function () { frm.refresh(); - } - - }) + }, + }); } From a6ddd86d31c2c2693e43be9a920810ffedeabb0e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 23 May 2022 15:25:00 +0200 Subject: [PATCH 050/192] fix: improve labels, simplify logic --- erpnext/stock/doctype/warehouse/warehouse.js | 47 ++++++++------------ 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js index 6baaf378fa..6a6ed1dffc 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.js +++ b/erpnext/stock/doctype/warehouse/warehouse.js @@ -33,34 +33,25 @@ frappe.ui.form.on("Warehouse", { }); }); - if (cint(frm.doc.is_group) == 1) { - frm.add_custom_button( - __("Group to Non-Group"), - function () { - convert_to_group_or_ledger(frm); - }, - "fa fa-retweet", - "btn-default" - ); - } else if (cint(frm.doc.is_group) == 0) { - if (frm.doc.__onload && frm.doc.__onload.account) { - frm.add_custom_button(__("General Ledger"), function () { - frappe.route_options = { - account: frm.doc.__onload.account, - company: frm.doc.company, - }; - frappe.set_route("query-report", "General Ledger"); - }); - } + frm.add_custom_button( + frm.doc.is_group + ? __("Convert to Ledger", null, "Warehouse") + : __("Convert to Group", null, "Warehouse"), + function () { + convert_to_group_or_ledger(frm); + }, + "fa fa-retweet", + "btn-default" + ); - frm.add_custom_button( - __("Non-Group to Group"), - function () { - convert_to_group_or_ledger(frm); - }, - "fa fa-retweet", - "btn-default" - ); + if (!frm.doc.is_group && frm.doc.__onload && frm.doc.__onload.account) { + frm.add_custom_button(__("General Ledger", null, "Warehouse"), function () { + frappe.route_options = { + account: frm.doc.__onload.account, + company: frm.doc.company, + }; + frappe.set_route("query-report", "General Ledger"); + }); } frm.toggle_enable(["is_group", "company"], false); @@ -84,7 +75,7 @@ frappe.ui.form.on("Warehouse", { filters: { is_group: 0, account_type: "Stock", - company: frm.doc.company, + company: doc.company, }, }; }; From 9356eb11de90f9675fced9d0e9828251d2b8845f Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 23 May 2022 15:35:15 +0200 Subject: [PATCH 051/192] fix: german translations --- erpnext/translations/de.csv | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 8730c4ecd3..61b6f05eea 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -1178,7 +1178,7 @@ Group by Party,Gruppieren nach Partei, Group by Voucher,Gruppieren nach Beleg, Group by Voucher (Consolidated),Gruppieren nach Beleg (konsolidiert), Group node warehouse is not allowed to select for transactions,Gruppenknoten Lager ist nicht für Transaktionen zu wählen erlaubt, -Group to Non-Group,Gruppe an konzernfremde, +Convert to Ledger,In Lagerbuch umwandeln,Warehouse Group your students in batches,Gruppieren Sie Ihre Schüler in den Reihen, Groups,Gruppen, Guardian1 Email ID,Guardian1 E-Mail-ID, @@ -1735,7 +1735,6 @@ Non GST Inward Supplies,Nicht GST Inward Supplies, Non Profit,Gemeinnützig, Non Profit (beta),Non-Profit (Beta), Non-GST outward supplies,Nicht-GST-Lieferungen nach außen, -Non-Group to Group,Non-Group-Gruppe, None,Keiner, None of the items have any change in quantity or value.,Keiner der Artikel hat irgendeine Änderung bei Mengen oder Kosten., Nos,Stk, From 2388d8662323cfc6de081a03244f68b3c880681c Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 23 May 2022 16:01:36 +0200 Subject: [PATCH 052/192] feat: Add german translations --- erpnext/translations/de.csv | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 8730c4ecd3..07fdd6589a 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -783,7 +783,7 @@ Default Activity Cost exists for Activity Type - {0},Es gibt Standard-Aktivität Default BOM ({0}) must be active for this item or its template,Standardstückliste ({0}) muss für diesen Artikel oder dessen Vorlage aktiv sein, Default BOM for {0} not found,Standardstückliste für {0} nicht gefunden, Default BOM not found for Item {0} and Project {1},Standard-Stückliste nicht gefunden für Position {0} und Projekt {1}, -Default In-Transit Warehouse, Standardlager für Waren im Transit, +Default In-Transit Warehouse,Standard-Durchgangslager, Default Letter Head,Standardbriefkopf, Default Tax Template,Standardsteuervorlage, Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM.,"Die Standard-Maßeinheit für Artikel {0} kann nicht direkt geändert werden, weil Sie bereits einige Transaktionen mit einer anderen Maßeinheit durchgeführt haben. Sie müssen einen neuen Artikel erstellen, um eine andere Standard-Maßeinheit verwenden zukönnen.", @@ -7653,7 +7653,7 @@ Campaign Schedules,Kampagnenpläne, Buyer of Goods and Services.,Käufer von Waren und Dienstleistungen., CUST-.YYYY.-,CUST-.YYYY.-, Default Company Bank Account,Standard-Bankkonto des Unternehmens, -From Lead,Von Lead, +From Lead,Aus Lead, Account Manager,Buchhalter, Allow Sales Invoice Creation Without Sales Order,Ermöglichen Sie die Erstellung von Kundenrechnungen ohne Auftrag, Allow Sales Invoice Creation Without Delivery Note,Ermöglichen Sie die Erstellung einer Ausgangsrechnung ohne Lieferschein, @@ -9852,3 +9852,24 @@ Row #{}: You must select {} serial numbers for item {}.,Zeile # {}: Sie müssen {} Available,{} Verfügbar, Report an Issue,Ein Problem melden, User Forum,Anwenderforum, +Get Customer Group Details,Einstellungen aus Kundengruppe übernehmen, +Is Rate Adjustment Entry (Debit Note),Ist Preisanpassung (Belastungsanzeige), +Fetch Timesheet,Zeiterfassung laden, +Company Tax ID,Eigene Steuernummer, +Quotation Number,Angebotsnummer, +Company Shipping Address,Eigene Lieferadresse, +Company Billing Address,Eigene Rechnungsadresse, +Billing Address Details,Vorschau Rechnungsadresse, +Supplier Contact,Lieferantenkontakt, +Order Status,Bestellstatus, +Invoice Portion (%),Rechnungsanteil (%), +Discount Settings,Rabatt-Einstellungen, +Payment Amount (Company Currency),Zahlungsbetrag (Unternehmenswährung), +Putaway Rule,Einlagerungsregel, +Apply Putaway Rule,Einlagerungsregel anwenden, +Default Discount Account,Standard-Rabattkonto, +Default Provisional Account,Standard Provisorisches Konto, +Leave Type Allocation,Zuordnung Abwesenheitsarten, +From Lead,Aus Lead, +From Opportunity,Aus Chance, +Publish in Website,Auf Webseite veröffentlichen, From ecb39d81e021f786f5a4ded3c344cf4f5c71bc26 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 23 May 2022 20:06:24 +0530 Subject: [PATCH 053/192] chore: error logging for auto material requests (#31103) --- erpnext/stock/reorder_item.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py index 4763b472c2..f19c75f54e 100644 --- a/erpnext/stock/reorder_item.py +++ b/erpnext/stock/reorder_item.py @@ -252,11 +252,14 @@ def notify_errors(exceptions_list): ) for exception in exceptions_list: - exception = json.loads(exception) - error_message = """
{0}

""".format( - _(exception.get("message")) - ) - content += error_message + try: + exception = json.loads(exception) + error_message = """
{0}

""".format( + _(exception.get("message")) + ) + content += error_message + except Exception: + pass content += _("Regards,") + "
" + _("Administrator") From 1ecb8f4a67c5a05291944b0275ec1a45a42a84e5 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 24 May 2022 14:32:56 +0530 Subject: [PATCH 054/192] chore: disable feed for material request --- erpnext/stock/doctype/material_request/material_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index c998629e76..2614a7f1f4 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -23,7 +23,7 @@ form_grid_templates = {"items": "templates/form_grid/material_request_grid.html" class MaterialRequest(BuyingController): def get_feed(self): - return _("{0}: {1}").format(self.status, self.material_request_type) + return def check_if_already_pulled(self): pass From e77c379cbbe9ae890efc6a652a9406540633e998 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Tue, 24 May 2022 11:52:23 +0200 Subject: [PATCH 055/192] fix: remove unsupported arguments Co-authored-by: Ankush Menat --- erpnext/stock/doctype/warehouse/warehouse.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js index 6a6ed1dffc..3d7f592153 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.js +++ b/erpnext/stock/doctype/warehouse/warehouse.js @@ -40,8 +40,6 @@ frappe.ui.form.on("Warehouse", { function () { convert_to_group_or_ledger(frm); }, - "fa fa-retweet", - "btn-default" ); if (!frm.doc.is_group && frm.doc.__onload && frm.doc.__onload.account) { From 1b16eb766791a9cd0f3c402efbf8f28a34922180 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 24 May 2022 13:30:59 +0200 Subject: [PATCH 056/192] refactor: set queries during setup --- erpnext/stock/doctype/warehouse/warehouse.js | 42 ++++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js index 3d7f592153..c902abf2e0 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.js +++ b/erpnext/stock/doctype/warehouse/warehouse.js @@ -2,13 +2,31 @@ // License: GNU General Public License v3. See license.txt frappe.ui.form.on("Warehouse", { - onload: function (frm) { - frm.set_query("default_in_transit_warehouse", function () { + setup: function (frm) { + frm.set_query("default_in_transit_warehouse", function (doc) { return { filters: { warehouse_type: "Transit", is_group: 0, - company: frm.doc.company, + company: doc.company, + }, + }; + }); + + frm.set_query("parent_warehouse", function () { + return { + filters: { + is_group: 1, + }, + }; + }); + + frm.set_query("account", function (doc) { + return { + filters: { + is_group: 0, + account_type: "Stock", + company: doc.company, }, }; }); @@ -59,24 +77,6 @@ frappe.ui.form.on("Warehouse", { fieldname: "name", doctype: "Warehouse", }; - - frm.fields_dict["parent_warehouse"].get_query = function (doc) { - return { - filters: { - is_group: 1, - }, - }; - }; - - frm.fields_dict["account"].get_query = function (doc) { - return { - filters: { - is_group: 0, - account_type: "Stock", - company: doc.company, - }, - }; - }; }, }); From 1e9f9c452f48bc2964d609e9f4a5e1a283519653 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 24 May 2022 13:31:29 +0200 Subject: [PATCH 057/192] style: format --- erpnext/stock/doctype/warehouse/warehouse.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js index c902abf2e0..d69c624fba 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.js +++ b/erpnext/stock/doctype/warehouse/warehouse.js @@ -61,13 +61,16 @@ frappe.ui.form.on("Warehouse", { ); if (!frm.doc.is_group && frm.doc.__onload && frm.doc.__onload.account) { - frm.add_custom_button(__("General Ledger", null, "Warehouse"), function () { - frappe.route_options = { - account: frm.doc.__onload.account, - company: frm.doc.company, - }; - frappe.set_route("query-report", "General Ledger"); - }); + frm.add_custom_button( + __("General Ledger", null, "Warehouse"), + function () { + frappe.route_options = { + account: frm.doc.__onload.account, + company: frm.doc.company, + }; + frappe.set_route("query-report", "General Ledger"); + } + ); } frm.toggle_enable(["is_group", "company"], false); From 55276f11f8d138c3a0748fe080ac68ddd1fd0f22 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Wed, 25 May 2022 00:05:22 -0400 Subject: [PATCH 058/192] fix: don't fetch item_code if already exists. (#31113) fix: check if item_code exists before fetching --- erpnext/stock/get_item_details.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 324ff4f409..c6241f8df6 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -199,7 +199,7 @@ def process_args(args): if not args.get("price_list"): args.price_list = args.get("selling_price_list") or args.get("buying_price_list") - if args.barcode: + if not args.item_code and args.barcode: args.item_code = get_item_code(barcode=args.barcode) elif not args.item_code and args.serial_no: args.item_code = get_item_code(serial_no=args.serial_no) From ab2d95a74d8beda1d751f7d795f37058826fff18 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 23 May 2022 13:00:00 +0530 Subject: [PATCH 059/192] feat: Level-wise BOM cost updation - Process BOMs level wise and Pause after level is complete - Cron job will resume Paused jobs, which will again process the new level and pause at the end - This will go on until all BOMs are updated - Added Progress section with fields to track updated BOMs in Log - Cleanup: Add BOM Updation utils file to contain helper functions/sub-functions - Cleanup: BOM Update Log file will only contain functions that are in direct context of the Log Co-authored-by: Gavin D'souza --- erpnext/hooks.py | 5 +- .../bom_update_log/bom_update_log.json | 29 ++- .../doctype/bom_update_log/bom_update_log.py | 169 ++++++------- .../bom_update_log/bom_updation_utils.py | 223 ++++++++++++++++++ .../bom_update_log/test_bom_update_log.py | 6 +- .../bom_update_tool/bom_update_tool.py | 102 +------- 6 files changed, 335 insertions(+), 199 deletions(-) create mode 100644 erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 813ac17ca0..05f06b3bda 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -392,9 +392,12 @@ after_migrate = ["erpnext.setup.install.update_select_perm_after_install"] scheduler_events = { "cron": { + "0/5 * * * *": [ + "erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs", + ], "0/30 * * * *": [ "erpnext.utilities.doctype.video.video.update_youtube_data", - ] + ], }, "all": [ "erpnext.projects.doctype.project.project.project_status_update_reminder", diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json index 98c1acb71c..3455b86657 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json @@ -13,6 +13,10 @@ "update_type", "status", "error_log", + "progress_section", + "current_boms", + "parent_boms", + "processed_boms", "amended_from" ], "fields": [ @@ -47,7 +51,7 @@ "fieldname": "status", "fieldtype": "Select", "label": "Status", - "options": "Queued\nIn Progress\nCompleted\nFailed" + "options": "Queued\nIn Progress\nPaused\nCompleted\nFailed" }, { "fieldname": "amended_from", @@ -63,13 +67,34 @@ "fieldtype": "Link", "label": "Error Log", "options": "Error Log" + }, + { + "fieldname": "progress_section", + "fieldtype": "Section Break", + "label": "Progress" + }, + { + "fieldname": "current_boms", + "fieldtype": "Text", + "label": "Current BOMs" + }, + { + "description": "Immediate parent BOMs", + "fieldname": "parent_boms", + "fieldtype": "Text", + "label": "Parent BOMs" + }, + { + "fieldname": "processed_boms", + "fieldtype": "Text", + "label": "Processed BOMs" } ], "in_create": 1, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-03-31 12:51:44.885102", + "modified": "2022-05-23 14:42:14.725914", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Update Log", diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index c0770fac90..639628ac38 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -1,13 +1,19 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from typing import Dict, List, Literal, Optional +import json +from typing import Dict, Optional import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cstr, flt +from frappe.utils import cstr -from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost +from erpnext.manufacturing.doctype.bom_update_log.bom_updation_utils import ( + get_leaf_boms, + handle_exception, + replace_bom, + set_values_in_log, +) class BOMMissingError(frappe.ValidationError): @@ -49,116 +55,93 @@ class BOMUpdateLog(Document): if self.update_type == "Replace BOM": boms = {"current_bom": self.current_bom, "new_bom": self.new_bom} frappe.enqueue( - method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.run_bom_job", + method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.run_replace_bom_job", doc=self, boms=boms, timeout=40000, ) else: - frappe.enqueue( - method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.run_bom_job", - doc=self, - update_type="Update Cost", - timeout=40000, - ) + process_boms_cost_level_wise(self) -def replace_bom(boms: Dict) -> None: - """Replace current BOM with new BOM in parent BOMs.""" - current_bom = boms.get("current_bom") - new_bom = boms.get("new_bom") - - unit_cost = get_new_bom_unit_cost(new_bom) - update_new_bom_in_bom_items(unit_cost, current_bom, new_bom) - - frappe.cache().delete_key("bom_children") - parent_boms = get_parent_boms(new_bom) - - for bom in parent_boms: - bom_obj = frappe.get_doc("BOM", bom) - # this is only used for versioning and we do not want - # to make separate db calls by using load_doc_before_save - # which proves to be expensive while doing bulk replace - bom_obj._doc_before_save = bom_obj - bom_obj.update_exploded_items() - bom_obj.calculate_cost() - bom_obj.update_parent_cost() - bom_obj.db_update() - if bom_obj.meta.get("track_changes") and not bom_obj.flags.ignore_version: - bom_obj.save_version() - - -def update_new_bom_in_bom_items(unit_cost: float, current_bom: str, new_bom: str) -> None: - bom_item = frappe.qb.DocType("BOM Item") - ( - frappe.qb.update(bom_item) - .set(bom_item.bom_no, new_bom) - .set(bom_item.rate, unit_cost) - .set(bom_item.amount, (bom_item.stock_qty * unit_cost)) - .where( - (bom_item.bom_no == current_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM") - ) - ).run() - - -def get_parent_boms(new_bom: str, bom_list: Optional[List] = None) -> List: - bom_list = bom_list or [] - bom_item = frappe.qb.DocType("BOM Item") - - parents = ( - frappe.qb.from_(bom_item) - .select(bom_item.parent) - .where((bom_item.bom_no == new_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM")) - .run(as_dict=True) - ) - - for d in parents: - if new_bom == d.parent: - frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(new_bom, d.parent)) - - bom_list.append(d.parent) - get_parent_boms(d.parent, bom_list) - - return list(set(bom_list)) - - -def get_new_bom_unit_cost(new_bom: str) -> float: - bom = frappe.qb.DocType("BOM") - new_bom_unitcost = ( - frappe.qb.from_(bom).select(bom.total_cost / bom.quantity).where(bom.name == new_bom).run() - ) - - return flt(new_bom_unitcost[0][0]) - - -def run_bom_job( +def run_replace_bom_job( doc: "BOMUpdateLog", boms: Optional[Dict[str, str]] = None, - update_type: Literal["Replace BOM", "Update Cost"] = "Replace BOM", ) -> None: try: doc.db_set("status", "In Progress") + if not frappe.flags.in_test: frappe.db.commit() frappe.db.auto_commit_on_many_writes = 1 - boms = frappe._dict(boms or {}) - - if update_type == "Replace BOM": - replace_bom(boms) - else: - update_cost() + replace_bom(boms) doc.db_set("status", "Completed") - except Exception: - frappe.db.rollback() - error_log = doc.log_error("BOM Update Tool Error") - - doc.db_set("status", "Failed") - doc.db_set("error_log", error_log.name) - + handle_exception(doc) finally: frappe.db.auto_commit_on_many_writes = 0 frappe.db.commit() # nosemgrep + + +def process_boms_cost_level_wise(update_doc: "BOMUpdateLog") -> None: + "Queue jobs at the start of new BOM Level in 'Update Cost' Jobs." + + current_boms, parent_boms = {}, [] + values = {} + + if update_doc.status == "Queued": + # First level yet to process. On Submit. + current_boms = {bom: False for bom in get_leaf_boms()} + values = { + "current_boms": json.dumps(current_boms), + "parent_boms": "[]", + "processed_boms": json.dumps({}), + "status": "In Progress", + } + else: + # status is Paused, resume. via Cron Job. + current_boms, parent_boms = json.loads(update_doc.current_boms), json.loads( + update_doc.parent_boms + ) + if not current_boms: + # Process the next level BOMs. Stage parents as current BOMs. + current_boms = {bom: False for bom in parent_boms} + values = { + "current_boms": json.dumps(current_boms), + "parent_boms": "[]", + "status": "In Progress", + } + + set_values_in_log(update_doc.name, values, commit=True) + queue_bom_cost_jobs(current_boms, update_doc) + + +def queue_bom_cost_jobs(current_boms: Dict, update_doc: "BOMUpdateLog") -> None: + "Queue batches of 20k BOMs of the same level to process parallelly" + current_boms_list = [bom for bom in current_boms] + + while current_boms_list: + boms_to_process = current_boms_list[:20000] # slice out batch of 20k BOMs + + # update list to exclude 20K (queued) BOMs + current_boms_list = current_boms_list[20000:] if len(current_boms_list) > 20000 else [] + frappe.enqueue( + method="erpnext.manufacturing.doctype.bom_update_log.bom_updation_utils.update_cost_in_level", + doc=update_doc, + bom_list=boms_to_process, + timeout=40000, + ) + + +def resume_bom_cost_update_jobs(): + "Called every 10 minutes via Cron job." + paused_jobs = frappe.db.get_all("BOM Update Log", {"status": "Paused"}) + if not paused_jobs: + return + + for job in paused_jobs: + # resume from next level + process_boms_cost_level_wise(update_doc=frappe.get_doc("BOM Update Log", job.name)) diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py new file mode 100644 index 0000000000..b5964cec9d --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py @@ -0,0 +1,223 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import json +from collections import defaultdict +from typing import TYPE_CHECKING, Dict, List, Optional + +if TYPE_CHECKING: + from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import BOMUpdateLog + +import frappe +from frappe import _ + + +def replace_bom(boms: Dict) -> None: + """Replace current BOM with new BOM in parent BOMs.""" + current_bom = boms.get("current_bom") + new_bom = boms.get("new_bom") + + unit_cost = get_bom_unit_cost(new_bom) + update_new_bom_in_bom_items(unit_cost, current_bom, new_bom) + + frappe.cache().delete_key("bom_children") + parent_boms = get_ancestor_boms(new_bom) + + for bom in parent_boms: + bom_obj = frappe.get_doc("BOM", bom) + # this is only used for versioning and we do not want + # to make separate db calls by using load_doc_before_save + # which proves to be expensive while doing bulk replace + bom_obj._doc_before_save = bom_obj + bom_obj.update_exploded_items() + bom_obj.calculate_cost() + bom_obj.update_parent_cost() + bom_obj.db_update() + if bom_obj.meta.get("track_changes") and not bom_obj.flags.ignore_version: + bom_obj.save_version() + + +def update_cost_in_level(doc: "BOMUpdateLog", bom_list: List[str]) -> None: + "Updates Cost for BOMs within a given level. Runs via background jobs." + try: + status = frappe.db.get_value("BOM Update Log", doc.name, "status") + if status == "Failed": + return + + frappe.db.auto_commit_on_many_writes = 1 + # main updation logic + job_data = update_cost_in_boms(bom_list=bom_list, docname=doc.name) + + set_values_in_log( + doc.name, + values={ + "current_boms": json.dumps(job_data.get("current_boms")), + "processed_boms": json.dumps(job_data.get("processed_boms")), + }, + commit=True, + ) + + process_if_level_is_complete(doc.name, job_data["current_boms"], job_data["processed_boms"]) + except Exception: + handle_exception(doc) + finally: + frappe.db.auto_commit_on_many_writes = 0 + frappe.db.commit() # nosemgrep + + +def get_ancestor_boms(new_bom: str, bom_list: Optional[List] = None) -> List: + bom_list = bom_list or [] + bom_item = frappe.qb.DocType("BOM Item") + + parents = ( + frappe.qb.from_(bom_item) + .select(bom_item.parent) + .where((bom_item.bom_no == new_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM")) + .run(as_dict=True) + ) + + for d in parents: + if new_bom == d.parent: + frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(new_bom, d.parent)) + + bom_list.append(d.parent) + get_ancestor_boms(d.parent, bom_list) + + return list(set(bom_list)) + + +def update_new_bom_in_bom_items(unit_cost: float, current_bom: str, new_bom: str) -> None: + bom_item = frappe.qb.DocType("BOM Item") + ( + frappe.qb.update(bom_item) + .set(bom_item.bom_no, new_bom) + .set(bom_item.rate, unit_cost) + .set(bom_item.amount, (bom_item.stock_qty * unit_cost)) + .where( + (bom_item.bom_no == current_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM") + ) + ).run() + + +def get_bom_unit_cost(new_bom: str) -> float: + bom = frappe.qb.DocType("BOM") + new_bom_unitcost = ( + frappe.qb.from_(bom).select(bom.total_cost / bom.quantity).where(bom.name == new_bom).run() + ) + + return frappe.utils.flt(new_bom_unitcost[0][0]) + + +def update_cost_in_boms(bom_list: List[str], docname: str) -> Dict: + "Updates cost in given BOMs. Returns current and total updated BOMs." + updated_boms = {} # current boms that have been updated + + for bom in bom_list: + bom_doc = frappe.get_cached_doc("BOM", bom) + bom_doc.calculate_cost(save_updates=True, update_hour_rate=True) + # bom_doc.update_exploded_items(save=True) #TODO: edit exploded items rate + bom_doc.db_update() + updated_boms[bom] = True + + # Update processed BOMs in Log + log_data = frappe.db.get_values( + "BOM Update Log", docname, ["current_boms", "processed_boms"], as_dict=True + )[0] + + for field in ("current_boms", "processed_boms"): + log_data[field] = json.loads(log_data.get(field)) + log_data[field].update(updated_boms) + + return log_data + + +def process_if_level_is_complete(docname: str, current_boms: Dict, processed_boms: Dict) -> None: + "Prepare and set higher level BOMs in Log if current level is complete." + processing_complete = all(current_boms.get(bom) for bom in current_boms) + if not processing_complete: + return + + parent_boms = get_next_higher_level_boms(child_boms=current_boms, processed_boms=processed_boms) + set_values_in_log( + docname, + values={ + "current_boms": json.dumps({}), + "parent_boms": json.dumps(parent_boms), + "status": "Completed" if not parent_boms else "Paused", + }, + commit=True, + ) + + +def get_next_higher_level_boms(child_boms: Dict, processed_boms: Dict): + "Generate immediate higher level dependants with no unresolved dependencies." + + def _all_children_are_processed(parent): + bom_doc = frappe.get_cached_doc("BOM", parent) + return all(processed_boms.get(row.bom_no) for row in bom_doc.items if row.bom_no) + + dependants_map = _generate_dependants_map() + dependants = set() + for bom in child_boms: + parents = dependants_map.get(bom) or [] + for parent in parents: + if _all_children_are_processed(parent): + dependants.add(parent) + + return list(dependants) + + +def get_leaf_boms(): + return frappe.db.sql_list( + """select name from `tabBOM` bom + where docstatus=1 and is_active=1 + and not exists(select bom_no from `tabBOM Item` + where parent=bom.name and ifnull(bom_no, '')!='')""" + ) + + +def _generate_dependants_map(): + bom = frappe.qb.DocType("BOM") + bom_item = frappe.qb.DocType("BOM Item") + + bom_parents = ( + frappe.qb.from_(bom_item) + .join(bom) + .on(bom_item.parent == bom.name) + .select(bom_item.bom_no, bom_item.parent) + .where( + (bom_item.bom_no.isnotnull()) + & (bom_item.bom_no != "") + & (bom.docstatus == 1) + & (bom.is_active == 1) + & (bom_item.parenttype == "BOM") + ) + ).run(as_dict=True) + + child_parent_map = defaultdict(list) + for bom in bom_parents: + child_parent_map[bom.bom_no].append(bom.parent) + + return child_parent_map + + +def set_values_in_log(log_name: str, values: Dict, commit: bool = False) -> None: + "Update BOM Update Log record." + if not values: + return + + bom_update_log = frappe.qb.DocType("BOM Update Log") + query = frappe.qb.update(bom_update_log).where(bom_update_log.name == log_name) + + for key, value in values.items(): + query = query.set(key, value) + query.run() + + if commit: + frappe.db.commit() + + +def handle_exception(doc: "BOMUpdateLog"): + frappe.db.rollback() + error_log = doc.log_error("BOM Update Tool Error") + set_values_in_log(doc.name, {"status": "Failed", "error_log": error_log.name}) diff --git a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py index 47efea961b..4f151334a2 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py @@ -6,7 +6,7 @@ from frappe.tests.utils import FrappeTestCase from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import ( BOMMissingError, - run_bom_job, + run_replace_bom_job, ) from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import enqueue_replace_bom @@ -71,7 +71,7 @@ class TestBOMUpdateLog(FrappeTestCase): # Explicitly commits log, new bom (setUp) and replacement impact. # Is run via background jobs IRL - run_bom_job( + run_replace_bom_job( doc=log, boms=self.boms, update_type="Replace BOM", @@ -88,7 +88,7 @@ class TestBOMUpdateLog(FrappeTestCase): log2 = enqueue_replace_bom( boms=self.boms, ) - run_bom_job( # Explicitly commits + run_replace_bom_job( # Explicitly commits doc=log2, boms=boms, update_type="Replace BOM", diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py index e765725340..4a2e03fb18 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -2,8 +2,7 @@ # For license information, please see license.txt import json -from collections import defaultdict -from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Union +from typing import TYPE_CHECKING, Dict, Literal, Optional, Union if TYPE_CHECKING: from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import BOMUpdateLog @@ -39,17 +38,7 @@ def enqueue_update_cost() -> "BOMUpdateLog": def auto_update_latest_price_in_all_boms() -> None: """Called via hooks.py.""" if frappe.db.get_single_value("Manufacturing Settings", "update_bom_costs_automatically"): - update_cost() - - -def update_cost() -> None: - """Updates Cost for all BOMs from bottom to top.""" - bom_list = get_boms_in_bottom_up_order() - for bom in bom_list: - bom_doc = frappe.get_cached_doc("BOM", bom) - bom_doc.calculate_cost(save_updates=True, update_hour_rate=True) - # bom_doc.update_exploded_items(save=True) #TODO: edit exploded items rate - bom_doc.db_update() + create_bom_update_log(update_type="Update Cost") def create_bom_update_log( @@ -69,90 +58,3 @@ def create_bom_update_log( "update_type": update_type, } ).submit() - - -def get_boms_in_bottom_up_order(bom_no: Optional[str] = None) -> List: - """ - Eg: Main BOM - |- Sub BOM 1 - |- Leaf BOM 1 - |- Sub BOM 2 - |- Leaf BOM 2 - Result: [Leaf BOM 1, Leaf BOM 2, Sub BOM 1, Sub BOM 2, Main BOM] - """ - leaf_boms = [] - if bom_no: - leaf_boms.append(bom_no) - else: - leaf_boms = _get_leaf_boms() - - child_parent_map = _generate_child_parent_map() - bom_list = leaf_boms.copy() - - for leaf_bom in leaf_boms: - parent_list = _get_flat_parent_map(leaf_bom, child_parent_map) - - if not parent_list: - continue - - bom_list.extend(parent_list) - bom_list = list(dict.fromkeys(bom_list).keys()) # remove duplicates - - return bom_list - - -def _generate_child_parent_map(): - bom = frappe.qb.DocType("BOM") - bom_item = frappe.qb.DocType("BOM Item") - - bom_parents = ( - frappe.qb.from_(bom_item) - .join(bom) - .on(bom_item.parent == bom.name) - .select(bom_item.bom_no, bom_item.parent) - .where( - (bom_item.bom_no.isnotnull()) - & (bom_item.bom_no != "") - & (bom.docstatus == 1) - & (bom.is_active == 1) - & (bom_item.parenttype == "BOM") - ) - ).run(as_dict=True) - - child_parent_map = defaultdict(list) - for bom in bom_parents: - child_parent_map[bom.bom_no].append(bom.parent) - - return child_parent_map - - -def _get_flat_parent_map(leaf, child_parent_map): - "Get ancestors at all levels of a leaf BOM." - parents_list = [] - - def _get_parents(node, parents_list): - "Returns recursively updated ancestors list." - first_parents = child_parent_map.get(node) # immediate parents of node - if not first_parents: # top most node - return parents_list - - parents_list.extend(first_parents) - parents_list = list(dict.fromkeys(parents_list).keys()) # remove duplicates - - for nth_node in first_parents: - # recursively find parents - parents_list = _get_parents(nth_node, parents_list) - - return parents_list - - parents_list = _get_parents(leaf, parents_list) - return parents_list - - -def _get_leaf_boms(): - return frappe.db.sql_list( - """select name from `tabBOM` bom - where docstatus=1 and is_active=1 - and not exists(select bom_no from `tabBOM Item` - where parent=bom.name and ifnull(bom_no, '')!='')""" - ) From 9f5f18e94da4254255a32d792abc94407ca5fde0 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 24 May 2022 18:17:40 +0530 Subject: [PATCH 060/192] style: Update docstrings and fix/add type hints + Collapsible progress section in Log --- .../bom_update_log/bom_update_log.json | 3 +- .../bom_update_log/bom_updation_utils.py | 45 ++++++++++++++----- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json index 3455b86657..db5f58d04f 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json @@ -69,6 +69,7 @@ "options": "Error Log" }, { + "collapsible": 1, "fieldname": "progress_section", "fieldtype": "Section Break", "label": "Progress" @@ -94,7 +95,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-05-23 14:42:14.725914", + "modified": "2022-05-24 17:52:21.824710", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Update Log", diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py index b5964cec9d..d246d3064f 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py @@ -3,7 +3,7 @@ import json from collections import defaultdict -from typing import TYPE_CHECKING, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional if TYPE_CHECKING: from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import BOMUpdateLog @@ -13,7 +13,8 @@ from frappe import _ def replace_bom(boms: Dict) -> None: - """Replace current BOM with new BOM in parent BOMs.""" + "Replace current BOM with new BOM in parent BOMs." + current_bom = boms.get("current_bom") new_bom = boms.get("new_bom") @@ -39,6 +40,7 @@ def replace_bom(boms: Dict) -> None: def update_cost_in_level(doc: "BOMUpdateLog", bom_list: List[str]) -> None: "Updates Cost for BOMs within a given level. Runs via background jobs." + try: status = frappe.db.get_value("BOM Update Log", doc.name, "status") if status == "Failed": @@ -66,6 +68,8 @@ def update_cost_in_level(doc: "BOMUpdateLog", bom_list: List[str]) -> None: def get_ancestor_boms(new_bom: str, bom_list: Optional[List] = None) -> List: + "Recursively get all ancestors of BOM." + bom_list = bom_list or [] bom_item = frappe.qb.DocType("BOM Item") @@ -99,17 +103,18 @@ def update_new_bom_in_bom_items(unit_cost: float, current_bom: str, new_bom: str ).run() -def get_bom_unit_cost(new_bom: str) -> float: +def get_bom_unit_cost(bom_name: str) -> float: bom = frappe.qb.DocType("BOM") new_bom_unitcost = ( - frappe.qb.from_(bom).select(bom.total_cost / bom.quantity).where(bom.name == new_bom).run() + frappe.qb.from_(bom).select(bom.total_cost / bom.quantity).where(bom.name == bom_name).run() ) return frappe.utils.flt(new_bom_unitcost[0][0]) -def update_cost_in_boms(bom_list: List[str], docname: str) -> Dict: +def update_cost_in_boms(bom_list: List[str], docname: str) -> Dict[str, Dict]: "Updates cost in given BOMs. Returns current and total updated BOMs." + updated_boms = {} # current boms that have been updated for bom in bom_list: @@ -131,8 +136,11 @@ def update_cost_in_boms(bom_list: List[str], docname: str) -> Dict: return log_data -def process_if_level_is_complete(docname: str, current_boms: Dict, processed_boms: Dict) -> None: - "Prepare and set higher level BOMs in Log if current level is complete." +def process_if_level_is_complete( + docname: str, current_boms: Dict[str, bool], processed_boms: Dict[str, bool] +) -> None: + "Prepare and set higher level BOMs/dependants in Log if current level is complete." + processing_complete = all(current_boms.get(bom) for bom in current_boms) if not processing_complete: return @@ -149,7 +157,9 @@ def process_if_level_is_complete(docname: str, current_boms: Dict, processed_bom ) -def get_next_higher_level_boms(child_boms: Dict, processed_boms: Dict): +def get_next_higher_level_boms( + child_boms: Dict[str, bool], processed_boms: Dict[str, bool] +) -> List[str]: "Generate immediate higher level dependants with no unresolved dependencies." def _all_children_are_processed(parent): @@ -167,7 +177,9 @@ def get_next_higher_level_boms(child_boms: Dict, processed_boms: Dict): return list(dependants) -def get_leaf_boms(): +def get_leaf_boms() -> List[str]: + "Get BOMs that have no dependencies." + return frappe.db.sql_list( """select name from `tabBOM` bom where docstatus=1 and is_active=1 @@ -176,7 +188,13 @@ def get_leaf_boms(): ) -def _generate_dependants_map(): +def _generate_dependants_map() -> defaultdict: + """ + Generate map such as: { BOM-1: [Dependant-BOM-1, Dependant-BOM-2, ..] }. + Here BOM-1 is the leaf/lower level node/dependency. + The list contains one level higher nodes/dependants that depend on BOM-1. + """ + bom = frappe.qb.DocType("BOM") bom_item = frappe.qb.DocType("BOM Item") @@ -201,8 +219,9 @@ def _generate_dependants_map(): return child_parent_map -def set_values_in_log(log_name: str, values: Dict, commit: bool = False) -> None: +def set_values_in_log(log_name: str, values: Dict[str, Any], commit: bool = False) -> None: "Update BOM Update Log record." + if not values: return @@ -217,7 +236,9 @@ def set_values_in_log(log_name: str, values: Dict, commit: bool = False) -> None frappe.db.commit() -def handle_exception(doc: "BOMUpdateLog"): +def handle_exception(doc: "BOMUpdateLog") -> None: + "Rolls back and fails BOM Update Log." + frappe.db.rollback() error_log = doc.log_error("BOM Update Tool Error") set_values_in_log(doc.name, {"status": "Failed", "error_log": error_log.name}) From 273b21c0cba764e7237bd8f43885dbc9b4c83304 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 25 May 2022 11:34:25 +0530 Subject: [PATCH 061/192] chore: move patch for updating Employee Advance status to v13 (#31118) --- erpnext/patches.txt | 2 +- .../patches/{v14_0 => v13_0}/update_employee_advance_status.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename erpnext/patches/{v14_0 => v13_0}/update_employee_advance_status.py (100%) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 4d9a7e06bf..8c0ebe7a90 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -359,7 +359,7 @@ erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr erpnext.patches.v13_0.update_accounts_in_loan_docs erpnext.patches.v14_0.update_batch_valuation_flag erpnext.patches.v14_0.delete_non_profit_doctypes -erpnext.patches.v14_0.update_employee_advance_status +erpnext.patches.v13_0.update_employee_advance_status erpnext.patches.v13_0.add_cost_center_in_loans erpnext.patches.v13_0.set_return_against_in_pos_invoice_references erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022 diff --git a/erpnext/patches/v14_0/update_employee_advance_status.py b/erpnext/patches/v13_0/update_employee_advance_status.py similarity index 100% rename from erpnext/patches/v14_0/update_employee_advance_status.py rename to erpnext/patches/v13_0/update_employee_advance_status.py From 268f413f56396a10f11559f8760b48f83fb6a7b4 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 25 May 2022 11:40:49 +0530 Subject: [PATCH 062/192] fix(pos): paid amount calculation for multicurrency invoice (#31112) --- .../public/js/controllers/taxes_and_totals.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 3dd11f69a7..16b0b4a866 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -789,11 +789,23 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)) { $.each(this.frm.doc['payments'] || [], function(index, data) { if(data.default && payment_status && total_amount_to_pay > 0) { - let base_amount = flt(total_amount_to_pay, precision("base_amount", data)); + let base_amount, amount; + + if (me.frm.doc.party_account_currency == me.frm.doc.currency) { + // if customer/supplier currency is same as company currency + // total_amount_to_pay is already in customer/supplier currency + // so base_amount has to be calculated using total_amount_to_pay + base_amount = flt(total_amount_to_pay * me.frm.doc.conversion_rate, precision("base_amount", data)); + amount = flt(total_amount_to_pay, precision("amount", data)); + } else { + base_amount = flt(total_amount_to_pay, precision("base_amount", data)); + amount = flt(total_amount_to_pay / me.frm.doc.conversion_rate, precision("amount", data)); + } + frappe.model.set_value(data.doctype, data.name, "base_amount", base_amount); - let amount = flt(total_amount_to_pay / me.frm.doc.conversion_rate, precision("amount", data)); frappe.model.set_value(data.doctype, data.name, "amount", amount); payment_status = false; + } else if(me.frm.doc.paid_amount) { frappe.model.set_value(data.doctype, data.name, "amount", 0.0); } From 6edbc5887311d454ebf487d080ad966d59f603ed Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 May 2022 11:51:07 +0530 Subject: [PATCH 063/192] fix: Handle missing HSN Codes (cherry picked from commit ce3a21eb039453d4786dfd0871a278b588039d80) --- erpnext/regional/report/gstr_1/gstr_1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index fd0fe26e88..0bdbe56de6 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -448,7 +448,7 @@ class Gstr1Report(object): hsn_code = self.item_hsn_map.get(item_code) tax_rate = 0 taxable_value = items.get(item_code) - for rates in hsn_wise_tax_rate.get(hsn_code): + for rates in hsn_wise_tax_rate.get(hsn_code, []): if taxable_value > rates.get("minimum_taxable_value"): tax_rate = rates.get("tax_rate") From 96d8b1ef3cb68011eb350386c229fb3fbcd067af Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 May 2022 14:19:10 +0530 Subject: [PATCH 064/192] feat: Auto accrue loan interest for backdated term loans --- erpnext/loan_management/doctype/loan/loan.py | 12 ++++++++++++ .../payroll/doctype/salary_slip/salary_slip.py | 16 ++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index a0ef1b971c..3b76ba4edb 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -68,6 +68,8 @@ class Loan(AccountsController): def on_submit(self): self.link_loan_security_pledge() + # Interest accrual for backdated term loans + self.accrue_loan_interest() def on_cancel(self): self.unlink_loan_security_pledge() @@ -187,6 +189,16 @@ class Loan(AccountsController): self.db_set("maximum_loan_amount", maximum_loan_value) + def accrue_loan_interest(self): + from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import ( + process_loan_interest_accrual_for_term_loans, + ) + + if getdate(self.repayment_start_date) < getdate() and self.is_term_loan: + process_loan_interest_accrual_for_term_loans( + posting_date=getdate(), loan_type=self.loan_type, loan=self.name + ) + def unlink_loan_security_pledge(self): pledges = frappe.get_all("Loan Security Pledge", fields=["name"], filters={"loan": self.name}) pledge_list = [d.name for d in pledges] diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 6a7f72b013..b55bfaa586 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -29,6 +29,9 @@ from erpnext.loan_management.doctype.loan_repayment.loan_repayment import ( calculate_amounts, create_repayment_entry, ) +from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import ( + process_loan_interest_accrual_for_term_loans, +) from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import ( get_benefit_component_amount, @@ -1364,9 +1367,9 @@ class SalarySlip(TransactionBase): self.total_loan_repayment += payment.total_payment def get_loan_details(self): - return frappe.get_all( + loan_details = frappe.get_all( "Loan", - fields=["name", "interest_income_account", "loan_account", "loan_type"], + fields=["name", "interest_income_account", "loan_account", "loan_type", "is_term_loan"], filters={ "applicant": self.employee, "docstatus": 1, @@ -1375,6 +1378,15 @@ class SalarySlip(TransactionBase): }, ) + if loan_details: + for loan in loan_details: + if loan.is_term_loan: + process_loan_interest_accrual_for_term_loans( + posting_date=self.posting_date, loan_type=loan.loan_type, loan=loan.name + ) + + return loan_details + def make_loan_repayment_entry(self): payroll_payable_account = get_payroll_payable_account(self.company, self.payroll_entry) for loan in self.loans: From bc3473770947ed5796a8e7d6fa718b2a55f326eb Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 May 2022 15:42:47 +0530 Subject: [PATCH 065/192] chore: Update test case --- erpnext/accounts/party.py | 15 +++++++++++++++ .../purchase_receipt/test_purchase_receipt.py | 9 +++++++++ 2 files changed, 24 insertions(+) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index db741d97e1..f4a44bd362 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -897,3 +897,18 @@ def get_default_contact(doctype, name): return None else: return None + + +def add_party_account(party_type, party, company, account): + doc = frappe.get_doc(party_type, party) + account_exists = False + for d in doc.get("accounts"): + if d.account == account: + account_exists = True + + if not account_exists: + accounts = {"company": company, "account": account} + + doc.append("accounts", accounts) + + doc.save() diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index ce3bd56d55..7fbfa62939 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1285,6 +1285,14 @@ class TestPurchaseReceipt(FrappeTestCase): from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import ( make_purchase_invoice as create_purchase_invoice, ) + from erpnext.accounts.party import add_party_account + + add_party_account( + "Supplier", + "_Test Supplier USD", + "_Test Company with perpetual inventory", + "_Test Payable USD - TCP1", + ) pi = create_purchase_invoice( company="_Test Company with perpetual inventory", @@ -1293,6 +1301,7 @@ class TestPurchaseReceipt(FrappeTestCase): expense_account="_Test Account Cost for Goods Sold - TCP1", currency="USD", conversion_rate=70, + supplier="_Test Supplier USD", ) pr = create_purchase_receipt(pi.name) From 147fc8fde704bc8c96dac2a24686cf7af5f7712b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 May 2022 17:58:30 +0530 Subject: [PATCH 066/192] fix: Loan Doc query in Bank Reconciliation Statement --- .../bank_reconciliation_statement.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py index f3ccc868c4..e5950b764e 100644 --- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py +++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py @@ -198,11 +198,12 @@ def get_loan_entries(filters): amount_field = (loan_doc.disbursed_amount).as_("credit") posting_date = (loan_doc.disbursement_date).as_("posting_date") account = loan_doc.disbursement_account + salary_condition = loan_doc.docstatus == 1 else: amount_field = (loan_doc.amount_paid).as_("debit") posting_date = (loan_doc.posting_date).as_("posting_date") account = loan_doc.payment_account - + salary_condition = loan_doc.repay_from_salary == 0 query = ( frappe.qb.from_(loan_doc) .select( @@ -214,15 +215,13 @@ def get_loan_entries(filters): posting_date, ) .where(loan_doc.docstatus == 1) + .where(salary_condition) .where(account == filters.get("account")) .where(posting_date <= getdate(filters.get("report_date"))) .where(ifnull(loan_doc.clearance_date, "4000-01-01") > getdate(filters.get("report_date"))) ) - if doctype == "Loan Repayment": - query.where(loan_doc.repay_from_salary == 0) - - entries = query.run(as_dict=1) + entries = query.run(as_dict=1, debug=1) loan_docs.extend(entries) return loan_docs @@ -267,15 +266,17 @@ def get_loan_amount(filters): amount_field = Sum(loan_doc.disbursed_amount) posting_date = (loan_doc.disbursement_date).as_("posting_date") account = loan_doc.disbursement_account + salary_condition = loan_doc.docstatus == 1 else: amount_field = Sum(loan_doc.amount_paid) posting_date = (loan_doc.posting_date).as_("posting_date") account = loan_doc.payment_account - + salary_condition = loan_doc.repay_from_salary == 0 amount = ( frappe.qb.from_(loan_doc) .select(amount_field) .where(loan_doc.docstatus == 1) + .where(salary_condition) .where(account == filters.get("account")) .where(posting_date > getdate(filters.get("report_date"))) .where(ifnull(loan_doc.clearance_date, "4000-01-01") <= getdate(filters.get("report_date"))) From a1f53f8d31aa22e56fded4b75c9d53839043c3d0 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 May 2022 17:59:40 +0530 Subject: [PATCH 067/192] chore: Linting Issues --- .../bank_reconciliation_statement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py index e5950b764e..5c70a404ef 100644 --- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py +++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py @@ -221,7 +221,7 @@ def get_loan_entries(filters): .where(ifnull(loan_doc.clearance_date, "4000-01-01") > getdate(filters.get("report_date"))) ) - entries = query.run(as_dict=1, debug=1) + entries = query.run(as_dict=1) loan_docs.extend(entries) return loan_docs From 9e4a36089eef11902f1f3c8b5759ab42e50b8b00 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 25 May 2022 18:10:42 +0530 Subject: [PATCH 068/192] chore: Linting Issues --- .../bank_reconciliation_statement.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py index 5c70a404ef..c41d0d10ff 100644 --- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py +++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py @@ -204,6 +204,7 @@ def get_loan_entries(filters): posting_date = (loan_doc.posting_date).as_("posting_date") account = loan_doc.payment_account salary_condition = loan_doc.repay_from_salary == 0 + query = ( frappe.qb.from_(loan_doc) .select( From 29228575faa94549b71846912e5dd52b3b8475f0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 25 May 2022 18:33:37 +0530 Subject: [PATCH 069/192] fix: Job Opening linked to Staffing Plan cannot be created/updated if there are existing employees --- erpnext/hr/doctype/job_opening/job_opening.py | 29 ++++++++++++------- .../hr/doctype/staffing_plan/staffing_plan.py | 21 ++++++-------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/erpnext/hr/doctype/job_opening/job_opening.py b/erpnext/hr/doctype/job_opening/job_opening.py index c71407d71d..f8e0c7d385 100644 --- a/erpnext/hr/doctype/job_opening/job_opening.py +++ b/erpnext/hr/doctype/job_opening/job_opening.py @@ -6,6 +6,7 @@ import frappe from frappe import _ +from frappe.utils import get_link_to_form from frappe.website.website_generator import WebsiteGenerator from erpnext.hr.doctype.staffing_plan.staffing_plan import ( @@ -33,26 +34,32 @@ class JobOpening(WebsiteGenerator): self.staffing_plan = staffing_plan[0].name self.planned_vacancies = staffing_plan[0].vacancies elif not self.planned_vacancies: - planned_vacancies = frappe.db.sql( - """ - select vacancies from `tabStaffing Plan Detail` - where parent=%s and designation=%s""", - (self.staffing_plan, self.designation), + self.planned_vacancies = frappe.db.get_value( + "Staffing Plan Detail", + {"parent": self.staffing_plan, "designation": self.designation}, + "vacancies", ) - self.planned_vacancies = planned_vacancies[0][0] if planned_vacancies else None if self.staffing_plan and self.planned_vacancies: staffing_plan_company = frappe.db.get_value("Staffing Plan", self.staffing_plan, "company") - lft, rgt = frappe.get_cached_value("Company", staffing_plan_company, ["lft", "rgt"]) - designation_counts = get_designation_counts(self.designation, self.company) + designation_counts = get_designation_counts(self.designation, self.company, self.name) current_count = designation_counts["employee_count"] + designation_counts["job_openings"] - if self.planned_vacancies <= current_count: + number_of_positions = frappe.db.get_value( + "Staffing Plan Detail", + {"parent": self.staffing_plan, "designation": self.designation}, + "number_of_positions", + ) + + if number_of_positions <= current_count: frappe.throw( _( - "Job Openings for designation {0} already open or hiring completed as per Staffing Plan {1}" - ).format(self.designation, self.staffing_plan) + "Job Openings for the designation {0} are already open or the hiring is completed as per the Staffing Plan {1}" + ).format( + frappe.bold(self.designation), get_link_to_form("Staffing Plan", self.staffing_plan) + ), + title=_("Vacancies fulfilled"), ) def get_context(self, context): diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.py b/erpnext/hr/doctype/staffing_plan/staffing_plan.py index ce7e50f7f4..82472dec41 100644 --- a/erpnext/hr/doctype/staffing_plan/staffing_plan.py +++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.py @@ -172,27 +172,24 @@ class StaffingPlan(Document): @frappe.whitelist() -def get_designation_counts(designation, company): +def get_designation_counts(designation, company, job_opening=None): if not designation: return False - employee_counts = {} company_set = get_descendants_of("Company", company) company_set.append(company) - employee_counts["employee_count"] = frappe.db.get_value( - "Employee", - filters={"designation": designation, "status": "Active", "company": ("in", company_set)}, - fieldname=["count(name)"], + employee_count = frappe.db.count( + "Employee", {"designation": designation, "status": "Active", "company": ("in", company_set)} ) - employee_counts["job_openings"] = frappe.db.get_value( - "Job Opening", - filters={"designation": designation, "status": "Open", "company": ("in", company_set)}, - fieldname=["count(name)"], - ) + filters = {"designation": designation, "status": "Open", "company": ("in", company_set)} + if job_opening: + filters["name"] = ("!=", job_opening) - return employee_counts + job_openings = frappe.db.count("Job Opening", filters) + + return {"employee_count": employee_count, "job_openings": job_openings} @frappe.whitelist() From c5e922c76bced414679fbaad33d9a50d0b2c62f4 Mon Sep 17 00:00:00 2001 From: xdlumertz Date: Wed, 25 May 2022 11:09:59 -0300 Subject: [PATCH 070/192] fix: Chart data for monthly periodicity in Cash Flow report (#31039) fix: Chart data for monthly periodicity in Cash Flow report --- erpnext/accounts/report/cash_flow/cash_flow.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/cash_flow/cash_flow.py b/erpnext/accounts/report/cash_flow/cash_flow.py index 74926b90ff..75e983afc0 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.py +++ b/erpnext/accounts/report/cash_flow/cash_flow.py @@ -262,7 +262,10 @@ def get_report_summary(summary_data, currency): def get_chart_data(columns, data): labels = [d.get("label") for d in columns[2:]] datasets = [ - {"name": account.get("account").replace("'", ""), "values": [account.get("total")]} + { + "name": account.get("account").replace("'", ""), + "values": [account.get(d.get("fieldname")) for d in columns[2:]], + } for account in data if account.get("parent_account") == None and account.get("currency") ] From ca75d814a30dd105099754a4f150c643941fb6b8 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 25 May 2022 12:12:35 +0530 Subject: [PATCH 071/192] fix: timesheet fetching in sales invoice (cherry picked from commit 216c32f4bc13376335ae31bd09fe6b5c01855ba2) --- .../doctype/sales_invoice/sales_invoice.js | 47 +++++++++++++------ 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index e30289a62c..9dde85fe12 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -861,27 +861,44 @@ frappe.ui.form.on('Sales Invoice', { set_timesheet_data: function(frm, timesheets) { frm.clear_table("timesheets") - timesheets.forEach(timesheet => { + timesheets.forEach(async (timesheet) => { if (frm.doc.currency != timesheet.currency) { - frappe.call({ - method: "erpnext.setup.utils.get_exchange_rate", - args: { - from_currency: timesheet.currency, - to_currency: frm.doc.currency - }, - callback: function(r) { - if (r.message) { - exchange_rate = r.message; - frm.events.append_time_log(frm, timesheet, exchange_rate); - } - } - }); + const exchange_rate = await frm.events.get_exchange_rate( + frm, timesheet.currency, frm.doc.currency + ) + frm.events.append_time_log(frm, timesheet, exchange_rate) } else { frm.events.append_time_log(frm, timesheet, 1.0); } }); }, + async get_exchange_rate(frm, from_currency, to_currency) { + if ( + frm.exchange_rates + && frm.exchange_rates[from_currency] + && frm.exchange_rates[from_currency][to_currency] + ) { + return frm.exchange_rates[from_currency][to_currency]; + } + + return frappe.call({ + method: "erpnext.setup.utils.get_exchange_rate", + args: { + from_currency, + to_currency + }, + callback: function(r) { + if (r.message) { + // cache exchange rates + frm.exchange_rates = frm.exchange_rates || {}; + frm.exchange_rates[from_currency] = frm.exchange_rates[from_currency] || {}; + frm.exchange_rates[from_currency][to_currency] = r.message; + } + } + }); + }, + append_time_log: function(frm, time_log, exchange_rate) { const row = frm.add_child("timesheets"); row.activity_type = time_log.activity_type; @@ -892,7 +909,7 @@ frappe.ui.form.on('Sales Invoice', { row.billing_hours = time_log.billing_hours; row.billing_amount = flt(time_log.billing_amount) * flt(exchange_rate); row.timesheet_detail = time_log.name; - row.project_name = time_log.project_name; + row.project_name = time_log.project_name; frm.refresh_field("timesheets"); frm.trigger("calculate_timesheet_totals"); From e5d2c1b1f3841a9df22cb770fd7ebbdfb3862779 Mon Sep 17 00:00:00 2001 From: sersaber <93864988+sersaber@users.noreply.github.com> Date: Thu, 26 May 2022 06:34:58 +0200 Subject: [PATCH 072/192] fix: change project's actual_start_date fieldtype from Data to Date (#31085) * Update project.json * Update project.json --- erpnext/projects/doctype/project/project.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index 1cda0a08c4..1790da44d6 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -234,7 +234,7 @@ }, { "fieldname": "actual_start_date", - "fieldtype": "Data", + "fieldtype": "Date", "label": "Actual Start Date (via Time Sheet)", "read_only": 1 }, @@ -458,7 +458,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 4, - "modified": "2022-01-29 13:58:27.712714", + "modified": "2022-05-25 22:45:06.108499", "modified_by": "Administrator", "module": "Projects", "name": "Project", @@ -504,4 +504,4 @@ "timeline_field": "customer", "title_field": "project_name", "track_seen": 1 -} \ No newline at end of file +} From ab0ef609188c232ae16e16b553ecac3a7e1bffcc Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 26 May 2022 11:32:46 +0530 Subject: [PATCH 073/192] test: Job Opening against a Staffing Plan --- .../doctype/job_opening/test_job_opening.py | 75 ++++++++++++++++++- .../staffing_plan/test_staffing_plan.py | 11 ++- 2 files changed, 79 insertions(+), 7 deletions(-) diff --git a/erpnext/hr/doctype/job_opening/test_job_opening.py b/erpnext/hr/doctype/job_opening/test_job_opening.py index a72a6eb338..e991054f62 100644 --- a/erpnext/hr/doctype/job_opening/test_job_opening.py +++ b/erpnext/hr/doctype/job_opening/test_job_opening.py @@ -3,8 +3,77 @@ import unittest -# test_records = frappe.get_test_records('Job Opening') +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days, getdate + +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.staffing_plan.test_staffing_plan import make_company -class TestJobOpening(unittest.TestCase): - pass +class TestJobOpening(FrappeTestCase): + def setUp(self): + frappe.db.delete("Staffing Plan") + frappe.db.delete("Staffing Plan Detail") + frappe.db.delete("Job Opening") + + make_company("_Test Opening Company", "_TOC") + frappe.db.delete("Employee", {"company": "_Test Opening Company"}) + + def test_vacancies_fulfilled(self): + make_employee( + "test_job_opening@example.com", company="_Test Opening Company", designation="Designer" + ) + + staffing_plan = frappe.get_doc( + { + "doctype": "Staffing Plan", + "company": "_Test Opening Company", + "name": "Test", + "from_date": getdate(), + "to_date": add_days(getdate(), 10), + } + ) + + staffing_plan.append( + "staffing_details", + {"designation": "Designer", "vacancies": 1, "estimated_cost_per_position": 50000}, + ) + staffing_plan.insert() + staffing_plan.submit() + + self.assertEqual(staffing_plan.staffing_details[0].number_of_positions, 2) + + # allows creating 1 job opening as per vacancy + opening_1 = get_job_opening() + opening_1.insert() + + # vacancies as per staffing plan already fulfilled via job opening and existing employee count + opening_2 = get_job_opening(job_title="Designer New") + self.assertRaises(frappe.ValidationError, opening_2.insert) + + # allows updating existing job opening + opening_1.status = "Closed" + opening_1.save() + + +def get_job_opening(**args): + args = frappe._dict(args) + + opening = frappe.db.exists("Job Opening", {"job_title": args.job_title or "Designer"}) + if opening: + return frappe.get_doc("Job Opening", opening) + + opening = frappe.get_doc( + { + "doctype": "Job Opening", + "job_title": "Designer", + "designation": "Designer", + "company": "_Test Opening Company", + "status": "Open", + } + ) + + opening.update(args) + + return opening diff --git a/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py b/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py index a3adbbd56a..ac69c21979 100644 --- a/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py +++ b/erpnext/hr/doctype/staffing_plan/test_staffing_plan.py @@ -85,13 +85,16 @@ def _set_up(): make_company() -def make_company(): - if frappe.db.exists("Company", "_Test Company 10"): +def make_company(name=None, abbr=None): + if not name: + name = "_Test Company 10" + + if frappe.db.exists("Company", name): return company = frappe.new_doc("Company") - company.company_name = "_Test Company 10" - company.abbr = "_TC10" + company.company_name = name + company.abbr = abbr or "_TC10" company.parent_company = "_Test Company 3" company.default_currency = "INR" company.country = "Pakistan" From 8d2502b5ba9dd158aa06c2e1b59235fcb29c687b Mon Sep 17 00:00:00 2001 From: Vladislav Date: Thu, 26 May 2022 09:18:38 +0300 Subject: [PATCH 074/192] chore: update ru translate --- erpnext/translations/ru.csv | 382 ++++++++++++++++++------------------ 1 file changed, 191 insertions(+), 191 deletions(-) diff --git a/erpnext/translations/ru.csv b/erpnext/translations/ru.csv index 6447546d22..6b766e7dc0 100644 --- a/erpnext/translations/ru.csv +++ b/erpnext/translations/ru.csv @@ -1,18 +1,18 @@ -"""Customer Provided Item"" cannot be Purchase Item also","«Товар, предоставленный клиентом» также не может быть предметом покупки", -"""Customer Provided Item"" cannot have Valuation Rate",«Предоставленный клиентом товар» не может иметь оценку, -"""Is Fixed Asset"" cannot be unchecked, as Asset record exists against the item","Нельзя отменить выбор ""Является основным средством"", поскольку по данному пункту имеется запись по активам", -'Based On' and 'Group By' can not be same,"""На основании"" и ""Группировка по"" не могут быть одинаковыми", -'Days Since Last Order' must be greater than or equal to zero,"""Дней с момента последнего заказа"" должно быть больше или равно 0", -'Entries' cannot be empty,"""Записи"" не могут быть пустыми", -'From Date' is required,"Поле ""С даты"" является обязательным для заполнения", -'From Date' must be after 'To Date',"Поле ""С даты"" должно быть после ""До даты""", -'Has Serial No' can not be 'Yes' for non-stock item,«Имеет серийный номер» не может быть «Да» для нескладируемого продукта, -'Opening',«Открывается», -'To Case No.' cannot be less than 'From Case No.',"«До дела №» не может быть меньше, чем «От дела №»", -'To Date' is required,"Поле ""До Даты"" является обязательным для заполнения", -'Total',«Итого», -'Update Stock' can not be checked because items are not delivered via {0},"Нельзя выбрать «Обновить запасы», так как продукты не поставляются через {0}", -'Update Stock' cannot be checked for fixed asset sale,"""Обновить запасы"" нельзя выбрать при продаже основных средств", +"""Customer Provided Item"" cannot be Purchase Item also","""Товар, предоставленный клиентом"" не может быть предметом покупки", +"""Customer Provided Item"" cannot have Valuation Rate","""Предоставленный клиентом товар"" не может иметь оценку", +"""Is Fixed Asset"" cannot be unchecked, as Asset record exists against the item","Нельзя убрать отметку ""Является основным средством"", поскольку по данному пункту имеется запись по активам", +'Based On' and 'Group By' can not be same,'На основании' и 'Группировка по' не могут быть одинаковыми, +'Days Since Last Order' must be greater than or equal to zero,'Дней с момента последнего заказа' должно быть больше или равно 0, +'Entries' cannot be empty,'Записи' не могут быть пустыми, +'From Date' is required,Поле 'С даты' является обязательным для заполнения, +'From Date' must be after 'To Date',Значение 'С даты' должно быть после 'До даты', +'Has Serial No' can not be 'Yes' for non-stock item,'Имеет серийный номер' не может быть 'Да' для нескладируемого продукта, +'Opening','Открытие', +'To Case No.' cannot be less than 'From Case No.',"'До дела №' не может быть меньше, чем 'От дела №'", +'To Date' is required,Поле 'До Даты' является обязательным для заполнения, +'Total','Итого', +'Update Stock' can not be checked because items are not delivered via {0},"Нельзя выбрать 'Обновить запасы', так как продукты не поставляются через {0}", +'Update Stock' cannot be checked for fixed asset sale,"'Обновить запасы' нельзя выбрать при продаже основных средств", ) for {0},) для {0}, 1 exact match.,1 точное совпадение., 90-Above,90-Над, @@ -290,7 +290,7 @@ Assign,Назначить, Assign Salary Structure,Назначить структуру заработной платы, Assign To,Назначить в, Assign to Employees,Назначить сотрудникам, -Assigning Structures...,Назначение структур ..., +Assigning Structures...,Назначение структур..., Associate,Помощник, At least one mode of payment is required for POS invoice.,По крайней мере один способ оплаты требуется для POS счета., Atleast one item should be entered with negative quantity in return document,Как минимум один продукт должен быть введен с отрицательным количеством в возвратном документе, @@ -702,12 +702,12 @@ Credit Account,Кредитный счет, Credit Balance,Кредитный баланс, Credit Card,Кредитная карта, Credit Days cannot be a negative number,Кредитные дни не могут быть отрицательным числом, -Credit Limit,{0}{/0} {1}Кредитный лимит {/1}, -Credit Note,Кредитная запись , +Credit Limit,Кредитный лимит, +Credit Note,Кредитная запись, Credit Note Amount,Сумма кредитной записи, Credit Note Issued,Кредит выдается справка, Credit Note {0} has been created automatically,Кредитная запись {0} была создана автоматически, -Credit limit has been crossed for customer {0} ({1}/{2}),Кредитный лимит был скрещен для клиента {0} ({1} / {2}), +Credit limit has been crossed for customer {0} ({1}/{2}),Кредитный лимит был скрещен для клиента {0} ({1}/{2}), Creditors,Кредиторы, Criteria weights must add up to 100%,Критерии веса должны составлять до 100%, Crop Cycle,Цикл урожая, @@ -892,19 +892,19 @@ Duplicate {0} found in the table,Дубликат {0} найден в табли Duration in Days,Продолжительность в днях, Duties and Taxes,Пошлины и налоги, E-Invoicing Information Missing,Отсутствует информация об инвойсировании, -ERPNext Demo,ERPNext Demo, +ERPNext Demo,ERPNext демо, ERPNext Settings,Настройки ERPNext, Earliest,Самый ранний, Earnest Money,Задаток, Earning,Зарабатывание, -Edit,Ред., +Edit,Редактировать, Edit Publishing Details,Редактировать информацию о публикации, "Edit in full page for more options like assets, serial nos, batches etc.","Редактируйте на полной странице дополнительные параметры, такие как активы, серийные номера, партии и т. Д.", Education,образование, Either location or employee must be required,"Требуется либо место, либо сотрудник", Either target qty or target amount is mandatory,Либо целевой Количество или целевое количество является обязательным, Either target qty or target amount is mandatory.,Либо целевой Количество или целевое количество является обязательным., -Electrical,электрический, +Electrical,Электрический, Electronic Equipments,Электронные приборы, Electronics,Электроника, Eligible ITC,Соответствующий ITC, @@ -1400,7 +1400,7 @@ Job card {0} created,Карта работы {0} создана, Jobs,Работы, Join,Присоединиться, Journal Entries {0} are un-linked,Записи в журнале {0} не-связаны, -Journal Entry,Запись в дневнике, +Journal Entry,Запись в журнале, Journal Entry {0} does not have account {1} or already matched against other voucher,Запись в журнале {0} не имеете учет {1} или уже сравнивается с другой ваучер, Kanban Board,Канбан-доска, Key Reports,Ключевые отчеты, @@ -1417,8 +1417,8 @@ Label,Ярлык, Laboratory,Лаборатория, Language Name,Название языка, Large,Большой, -Last Communication,Последнее сообщение, -Last Communication Date,Дата последнего общения, +Last Communication,Последняя коммуникация, +Last Communication Date,Дата последней коммуникации, Last Name,Фамилия, Last Order Amount,Последняя сумма заказа, Last Order Date,Последняя дата заказа, @@ -1449,12 +1449,12 @@ Leave and Attendance,Оставить и посещаемость, Leave application {0} already exists against the student {1},Оставить заявку {0} уже существует против ученика {1}, "Leave cannot be allocated before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}","Оставить не могут быть выделены, прежде чем {0}, а отпуск баланс уже переноса направляются в будущем записи распределения отпуска {1}", "Leave cannot be applied/cancelled before {0}, as leave balance has already been carry-forwarded in the future leave allocation record {1}","Оставьте не могут быть применены / отменены, прежде чем {0}, а отпуск баланс уже переноса направляются в будущем записи распределения отпуска {1}", -Leave of type {0} cannot be longer than {1},"Оставить типа {0} не может быть больше, чем {1}", -Leaves,Листья, +Leave of type {0} cannot be longer than {1},"Отпусков типа {0} не может быть больше, чем {1}", +Leaves,Отпуски, Leaves Allocated Successfully for {0},Отпуск успешно распределен для {0}, -Leaves has been granted sucessfully,Листья были успешно предоставлены, -Leaves must be allocated in multiples of 0.5,"Листья должны быть выделены несколько 0,5", -Leaves per Year,Листья в год, +Leaves has been granted sucessfully,Отпуска были успешно предоставлены, +Leaves must be allocated in multiples of 0.5,"Отпуска должны быть распределены кратно 0,5", +Leaves per Year,Отпусков в год, Ledger,Регистр, Legal,Легальный, Legal Expenses,Судебные издержки, @@ -1480,7 +1480,7 @@ Loans (Liabilities),Кредиты (обязательства), Loans and Advances (Assets),Кредиты и авансы (активы), Local,Локальные, Log,Запись в журнале, -Logs for maintaining sms delivery status,Журналы для просмотра статуса доставки СМС, +Logs for maintaining sms delivery status,Журналы для просмотра статуса доставки SMS, Lost,Поражений, Lost Reasons,Потерянные причины, Low,Низкий, @@ -1514,7 +1514,7 @@ Manage Customer Group Tree.,Управление деревом групп по Manage Sales Partners.,Управление партнерами по сбыту., Manage Sales Person Tree.,Управление деревом менеджеров по продажам., Manage Territory Tree.,Управление деревом территорий., -Manage your orders,Управляйте свои заказы, +Manage your orders,Управление вашими заказами, Management,Менеджмент, Manager,Менеджер, Managing Projects,Управление проектами, @@ -1577,10 +1577,10 @@ Meeting,Встреча, Member Activity,Активность участника, Member ID,ID пользователя, Member Name,Имя участника, -Member information.,Информация о членах., +Member information.,Информация об участниках., Membership,Членство, Membership Details,Сведения о членстве, -Membership ID,Идентификатор членства, +Membership ID,Идентификатор участника, Membership Type,Тип членства, Memebership Details,Меморандум, Memebership Type Details,Информация о типе памяти, @@ -1939,7 +1939,7 @@ Pharmaceutical,Фармацевтический, Pharmaceuticals,Фармацевтика, Physician,Врач, Piecework,Сдельная работа, -Pincode,Pincode, +Pincode,PIN код, Place Of Supply (State/UT),Место поставки (штат / UT), Place Order,Разместить заказ, Plan Name,Название плана, @@ -1953,7 +1953,7 @@ Please add a Temporary Opening account in Chart of Accounts,"Пожалуйст Please add the account to root level Company - ,"Пожалуйста, добавьте счет на корневой уровень компании -", Please add the remaining benefits {0} to any of the existing component,Добавьте оставшиеся преимущества {0} к любому из существующих компонентов, Please check Multi Currency option to allow accounts with other currency,"Пожалуйста, проверьте мультивалютный вариант, позволяющий счета другой валюте", -Please click on 'Generate Schedule',"Пожалуйста, нажмите на кнопку ""Создать расписание""", +Please click on 'Generate Schedule',"Пожалуйста, нажмите на кнопку 'Создать расписание'", Please click on 'Generate Schedule' to fetch Serial No added for Item {0},"Пожалуйста, нажмите на кнопку ""Создать расписание"", чтобы принести Серийный номер добавлен для Пункт {0}", Please click on 'Generate Schedule' to get schedule,"Пожалуйста, нажмите на кнопку ""Создать расписание"", чтобы получить график", Please confirm once you have completed your training,"Пожалуйста, подтвердите, как только вы закончили обучение", @@ -1963,7 +1963,7 @@ Please enable Applicable on Booking Actual Expenses,"Пожалуйста, вк Please enable Applicable on Purchase Order and Applicable on Booking Actual Expenses,"Пожалуйста, включите Применимо по заказу на поставку и применимо при бронировании Фактические расходы", Please enable default incoming account before creating Daily Work Summary Group,"Включите учетную запись по умолчанию, прежде чем создавать сводную группу ежедневных работ", Please enable pop-ups,"Пожалуйста, включите всплывающие окна", -Please enter 'Is Subcontracted' as Yes or No,"Пожалуйста, введите 'Является субподряду "", как Да или Нет", +Please enter 'Is Subcontracted' as Yes or No,"Пожалуйста, введите 'Является субподрядом', как Да или Нет", Please enter API Consumer Key,Введите API-адрес потребителя, Please enter API Consumer Secret,"Пожалуйста, введите секретный раздел API", Please enter Account for Change Amount,"Пожалуйста, введите счет для изменения высоты", @@ -2122,7 +2122,7 @@ Point of Sale,Точки продаж, Point-of-Sale,Торговая точка, Point-of-Sale Profile,Точка-в-продажи профиля, Portal,Портал, -Portal Settings,портал Настройки, +Portal Settings,Настройки портала, Possible Supplier,Возможный поставщик, Postal Expenses,Почтовые расходы, Posting Date,Дата публикации, @@ -2185,7 +2185,7 @@ Produced Qty,Произведенное количество, Product,Продукт, Product Bundle,Продуктовый набор, Product Search,Поиск продукта, -Production,производство, +Production,Производство, Production Item,Производство товара, Products,Продукты, Profit and Loss,Прибыль и убытки, @@ -2195,7 +2195,7 @@ Program in the Fee Structure and Student Group {0} are different.,Програм Program {0} does not exist.,Программа {0} не существует., Program: ,Программа: , Progress % for a task cannot be more than 100.,Готовность задачи не может превышать 100%., -Project Collaboration Invitation,Сотрудничество Приглашение проекта, +Project Collaboration Invitation,Приглашение к сотрудничеству в проекте, Project Id,Идентификатор проекта, Project Manager,Менеджер проектов, Project Name,Название проекта, @@ -2230,7 +2230,7 @@ Purchase Manager,Менеджер поставок, Purchase Master Manager,Руководитель поставок, Purchase Order,Заказ на покупку, Purchase Order Amount,Сумма заказа на покупку, -Purchase Order Amount(Company Currency),Сумма заказа на покупку (валюта компании), +Purchase Order Amount(Company Currency),Сумма заказа на покупку (в валюте компании), Purchase Order Date,Дата заказа на покупку, Purchase Order Items not received on time,Элементы заказа на поставку не принимаются вовремя, Purchase Order number required for Item {0},Число Заказ требуется для продукта {0}, @@ -2296,7 +2296,7 @@ Raw Materials,Сырье, Raw Materials cannot be blank.,Сырье не может быть пустым., Re-open,Снова откройте, Read blog,Читать блог, -Read the ERPNext Manual,Прочитайте Руководство ERPNext, +Read the ERPNext Manual,Прочитайте руководство ERPNext, Reading Uploaded File,Чтение загруженного файла, Real Estate,Недвижимость, Reason For Putting On Hold,Причина удержания, @@ -2429,37 +2429,37 @@ Row # {0}: Cannot return more than {1} for Item {2},Строка # {0}: Нево Row # {0}: Rate cannot be greater than the rate used in {1} {2},"Строка # {0}: ставка не может быть больше ставки, используемой в {1} {2}", Row # {0}: Serial No is mandatory,Строка # {0}: Серийный номер является обязательным, Row # {0}: Serial No {1} does not match with {2} {3},"Строка # {0}: Серийный номер {1}, не соответствует {2} {3}", -Row #{0} (Payment Table): Amount must be negative,Строка # {0} (таблица платежей): сумма должна быть отрицательной, -Row #{0} (Payment Table): Amount must be positive,Строка # {0} (таблица платежей): сумма должна быть положительной, -Row #{0}: Account {1} does not belong to company {2},Строка # {0}: Счет {1} не принадлежит компании {2}, -Row #{0}: Allocated Amount cannot be greater than outstanding amount.,Строка # {0}: выделенная сумма не может превышать невыплаченную сумму., -"Row #{0}: Asset {1} cannot be submitted, it is already {2}","Строка # {0}: Актив {1} не может быть проведен, он уже {2}", -Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.,"Строка # {0}: не может установить значение скорости, если сумма превышает сумму выставленного счета за элемент {1}.", -Row #{0}: Clearance date {1} cannot be before Cheque Date {2},Строка # {0}: дате зазора {1} не может быть до того Cheque Дата {2}, -Row #{0}: Duplicate entry in References {1} {2},Строка # {0}: Дублирующая запись в ссылках {1} {2}, -Row #{0}: Expected Delivery Date cannot be before Purchase Order Date,Строка # {0}: ожидаемая дата поставки не может быть до даты заказа на поставку, -Row #{0}: Item added,Строка № {0}: пункт добавлен, -Row #{0}: Journal Entry {1} does not have account {2} or already matched against another voucher,Строка # {0}: Запись в журнале {1} не имеет учетной записи {2} или уже сопоставляется с другой купон, -Row #{0}: Not allowed to change Supplier as Purchase Order already exists,Строка # {0}: Не разрешено изменять поставщика когда уже существует заказ, -Row #{0}: Please set reorder quantity,"Строка # {0}: Пожалуйста, укажите количество повторного заказа", -Row #{0}: Please specify Serial No for Item {1},"Строка # {0}: Пожалуйста, сформулируйте серийный номер для продукта {1}", -Row #{0}: Qty increased by 1,Строка № {0}: кол-во увеличено на 1, -Row #{0}: Rate must be same as {1}: {2} ({3} / {4}) ,"Строка # {0}: цена должна быть такой же, как {1}: {2} ({3} / {4})", -Row #{0}: Reference Document Type must be one of Expense Claim or Journal Entry,Строка # {0}: Тип ссылочного документа должен быть одним из заголовка расхода или записи журнала, -"Row #{0}: Reference Document Type must be one of Purchase Order, Purchase Invoice or Journal Entry","Строка # {0}: Тип справочного документа должен быть одним из следующих: Заказ на покупку, Счет-фактура на покупку или Запись в журнале", -Row #{0}: Rejected Qty can not be entered in Purchase Return,Строка # {0}: Отклоненное количество не может быть введено в возврат покупки, -Row #{0}: Rejected Warehouse is mandatory against rejected Item {1},Строка # {0}: Отклонено Склад является обязательным в отношении отклонил Пункт {1}, -Row #{0}: Reqd by Date cannot be before Transaction Date,Строка # {0}: Reqd by Date не может быть до даты транзакции, -Row #{0}: Set Supplier for item {1},Строка # {0}: Установить поставщика для {1}, -Row #{0}: Status must be {1} for Invoice Discounting {2},Строка # {0}: статус должен быть {1} для дисконтирования счета-фактуры {2}, +Row #{0} (Payment Table): Amount must be negative,Строка #{0} (таблица платежей): сумма должна быть отрицательной, +Row #{0} (Payment Table): Amount must be positive,Строка #{0} (таблица платежей): сумма должна быть положительной, +Row #{0}: Account {1} does not belong to company {2},Строка #{0}: Счет {1} не принадлежит компании {2}, +Row #{0}: Allocated Amount cannot be greater than outstanding amount.,Строка #{0}: выделенная сумма не может превышать невыплаченную сумму., +"Row #{0}: Asset {1} cannot be submitted, it is already {2}","Строка #{0}: Актив {1} не может быть проведен, он уже {2}", +Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.,"Строка #{0}: не может установить значение скорости, если сумма превышает сумму выставленного счета за элемент {1}.", +Row #{0}: Clearance date {1} cannot be before Cheque Date {2},Строка #{0}: дате зазора {1} не может быть до того Cheque Дата {2}, +Row #{0}: Duplicate entry in References {1} {2},Строка #{0}: Дублирующая запись в ссылках {1} {2}, +Row #{0}: Expected Delivery Date cannot be before Purchase Order Date,Строка #{0}: ожидаемая дата поставки не может быть до даты заказа на поставку, +Row #{0}: Item added,Строка #{0}: пункт добавлен, +Row #{0}: Journal Entry {1} does not have account {2} or already matched against another voucher,Строка #{0}: Запись в журнале {1} не имеет учетной записи {2} или уже сопоставляется с другой купон, +Row #{0}: Not allowed to change Supplier as Purchase Order already exists,Строка #{0}: Не разрешено изменять поставщика когда уже существует заказ, +Row #{0}: Please set reorder quantity,"Строка #{0}: Пожалуйста, укажите количество повторных заказов", +Row #{0}: Please specify Serial No for Item {1},"Строка #{0}: Пожалуйста, сформулируйте серийный номер для продукта {1}", +Row #{0}: Qty increased by 1,Строка #{0}: кол-во увеличено на 1, +Row #{0}: Rate must be same as {1}: {2} ({3} / {4}) ,"Строка #{0}: цена должна быть такой же, как {1}: {2} ({3} / {4})", +Row #{0}: Reference Document Type must be one of Expense Claim or Journal Entry,Строка #{0}: Тип ссылочного документа должен быть одним из заголовка расхода или записи журнала, +"Row #{0}: Reference Document Type must be one of Purchase Order, Purchase Invoice or Journal Entry","Строка #{0}: Тип справочного документа должен быть одним из следующих: Заказ на покупку, Счет-фактура на покупку или Запись в журнале", +Row #{0}: Rejected Qty can not be entered in Purchase Return,Строка #{0}: Отклоненное количество не может быть введено в возврат покупки, +Row #{0}: Rejected Warehouse is mandatory against rejected Item {1},Строка #{0}: Отклоненный склад является обязательным для отклоненного продукта {1}, +Row #{0}: Reqd by Date cannot be before Transaction Date,Строка #{0}: Reqd by Date не может быть до даты транзакции, +Row #{0}: Set Supplier for item {1},Строка #{0}: Установить поставщика для {1}, +Row #{0}: Status must be {1} for Invoice Discounting {2},Строка #{0}: статус должен быть {1} для дисконтирования счета-фактуры {2}, "Row #{0}: The batch {1} has only {2} qty. Please select another batch which has {3} qty available or split the row into multiple rows, to deliver/issue from multiple batches","Строка # {0}: партия {1} имеет только {2} qty. Выберите другой пакет, в котором имеется {3} qty, или разбейте строку на несколько строк, чтобы доставлять / выпускать из нескольких партий", -Row #{0}: Timings conflicts with row {1},Строка # {0}: Тайминги конфликтуют со строкой {1}, -Row #{0}: {1} can not be negative for item {2},Строка # {0}: {1} не может быть отрицательным для {2}, -Row No {0}: Amount cannot be greater than Pending Amount against Expense Claim {1}. Pending Amount is {2},"Строка № {0}: Сумма не может быть больше, чем указанная в Авансовом Отчете {1}. Указанная сумма {2}", +Row #{0}: Timings conflicts with row {1},Строка #{0}: Тайминги конфликтуют со строкой {1}, +Row #{0}: {1} can not be negative for item {2},Строка #{0}: {1} не может быть отрицательным для {2}, +Row No {0}: Amount cannot be greater than Pending Amount against Expense Claim {1}. Pending Amount is {2},"Строка #{0}: Сумма не может быть больше, чем указанная в Авансовом Отчете {1}. Указанная сумма {2}", Row {0} : Operation is required against the raw material item {1},Строка {0}: требуется операция против элемента исходного материала {1}, -Row {0}# Allocated amount {1} cannot be greater than unclaimed amount {2},"Строка {0} # Выделенная сумма {1} не может быть больше, чем невостребованная сумма {2}", -Row {0}# Item {1} cannot be transferred more than {2} against Purchase Order {3},Строка {0} # Элемент {1} не может быть передан более {2} в отношении заказа на поставку {3}, -Row {0}# Paid Amount cannot be greater than requested advance amount,Строка {0} # Платная сумма не может быть больше запрашиваемой суммы аванса, +Row {0}# Allocated amount {1} cannot be greater than unclaimed amount {2},"Строка {0}# Выделенная сумма {1} не может быть больше, чем невостребованная сумма {2}", +Row {0}# Item {1} cannot be transferred more than {2} against Purchase Order {3},Строка {0}# Продукт {1} не может быть передан более {2} в отношении заказа на поставку {3}, +Row {0}# Paid Amount cannot be greater than requested advance amount,Строка {0}# Оплаченная сумма не может быть больше запрошенной суммы аванса, Row {0}: Activity Type is mandatory.,Строка {0}: Вид деятельности является обязательным., Row {0}: Advance against Customer must be credit,Строка {0}: Аванс в отношении клиента должен быть кредитом, Row {0}: Advance against Supplier must be debit,Строка {0}: Аванс в отношении поставщика должны быть дебетом, @@ -2695,7 +2695,7 @@ Settings for website homepage,Настройки для сайта домашн Settings for website product listing,Настройки для списка товаров на сайте, Settled,Установившаяся, Setup Gateway accounts.,Настройка шлюза счета., -Setup SMS gateway settings,Указать настройки СМС-шлюза, +Setup SMS gateway settings,Указать настройки SMS-шлюза, Setup cheque dimensions for printing,Размеры Проверьте настройки для печати, Setup default values for POS Invoices,Настройка значений по умолчанию для счетов POS, Setup mode of POS (Online / Offline),Режим настройки POS (Online / Offline), @@ -3035,7 +3035,7 @@ To Date cannot be before From Date,На сегодняшний день не м To Date cannot be less than From Date,"Дата не может быть меньше, чем с даты", To Date must be greater than From Date,"До даты должно быть больше, чем с даты", "To Date should be within the Fiscal Year. Assuming To Date = {0}","Дата должна быть в пределах финансового года. Предположим, до даты = {0}", -To Datetime,Для DateTime, +To Datetime,Ко времени, To Deliver,Для доставки, To Deliver and Bill,Для доставки и оплаты, To Fiscal Year,К финансовому году, @@ -3043,10 +3043,10 @@ To GSTIN,К GSTIN, To Party Name,Название партии, To Pin Code,К PIN-коду, To Place,Положить, -To Receive,Получить, +To Receive,К получению, To Receive and Bill,Для приема и Билл, To State,Государство, -To Warehouse,Для Склад, +To Warehouse,Для склада, To create a Payment Request reference document is required,Для создания ссылочного документа запроса платежа требуется, To date can not be equal or less than from date,"На сегодняшний день не может быть равным или меньше, чем с даты", To date can not be less than from date,"На сегодняшний день не может быть меньше, чем с даты", @@ -3081,7 +3081,7 @@ Total Credit/ Debit Amount should be same as linked Journal Entry,"Общая с Total Debit must be equal to Total Credit. The difference is {0},"Всего Дебет должна быть равна общей выработке. Разница в том, {0}", Total Deduction,Общий вычет, Total Invoiced Amount,Общая сумма по счетам, -Total Leaves,Всего Листья, +Total Leaves,Всего отпусков, Total Order Considered,Всего рассмотренных заказов, Total Order Value,Общая стоимость заказа, Total Outgoing,Всего исходящих, @@ -3458,7 +3458,7 @@ on,вкл, {0} {1}: Customer is required against Receivable account {2},{0} {1}: Наименование клиента обязательно для Дебиторской задолженности {2}, {0} {1}: Either debit or credit amount is required for {2},{0} {1}: Требуется указать сумму дебета или кредита для {2}, {0} {1}: Supplier is required against Payable account {2},{0} {1}: Наименование поставщика обязательно для кредиторской задолженности {2}, -{0}% Billed,{0} % оплачено, +{0}% Billed,{0}% оплачено, {0}% Delivered,{0}% доставлено, "{0}: Employee email not found, hence email not sent","{0}: Адрес электронной почты сотрудника не найден, поэтому письмо не отправлено", {0}: From {0} of type {1},{0}: От {0} типа {1}, @@ -3472,7 +3472,7 @@ Completed By,Завершено, Conditions,Условия, County,Округ, Day of Week,День недели, -"Dear System Manager,","Уважаемый Менеджер системы,", +"Dear System Manager,","Уважаемый менеджер системы,", Default Value,Значение по умолчанию, Email Group,Группа электронной почты, Email Settings,Настройки электронной почты, @@ -3537,7 +3537,7 @@ Quality Feedback Template,Шаблон обратной связи по каче Rules for applying different promotional schemes.,Правила применения разных рекламных схем., Shift,Сдвиг, Show {0},Показать {0}, -"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Специальные символы, кроме "-", "#", ".", "/", "{" И "}", не допускаются в именных сериях", +"Special Characters except ""-"", ""#"", ""."", ""/"", ""{"" and ""}"" not allowed in naming series","Специальные символы, кроме ""-"", ""#"", ""."", ""/"", ""{"" и ""}"", не допускаются в серийных номерах", Target Details,Детали цели, {0} already has a Parent Procedure {1}.,{0} уже имеет родительскую процедуру {1}., API,API, @@ -3660,7 +3660,7 @@ Chart,Диаграмма, Choose a corresponding payment,Выберите соответствующий платеж, Click on the link below to verify your email and confirm the appointment,"Нажмите на ссылку ниже, чтобы подтвердить свою электронную почту и подтвердить встречу", Close,Закрыть, -Communication,Общение, +Communication,Коммуникация, Compact Item Print,Компактный товара печати, Company,Организация, Company of asset {0} and purchase document {1} doesn't matches.,Компания актива {0} и документ покупки {1} не совпадают., @@ -3681,8 +3681,8 @@ Create New Contact,Создать новый контакт, Create New Lead,Создать новый лид, Create Pick List,Создать список выбора, Create Quality Inspection for Item {0},Создать проверку качества для позиции {0}, -Creating Accounts...,Создание аккаунтов ..., -Creating bank entries...,Создание банковских записей ..., +Creating Accounts...,Создание счетов..., +Creating bank entries...,Создание банковских записей..., Credit limit is already defined for the Company {0},Кредитный лимит уже определен для Компании {0}, Ctrl + Enter to submit,Ctrl + Enter для отправки, Ctrl+Enter to submit,Ctrl + Enter для отправки, @@ -3694,7 +3694,7 @@ Daily,Ежедневно, Date,Дата, Date Range,Диапазон дат, Date of Birth cannot be greater than Joining Date.,Дата рождения не может быть больше даты присоединения., -Dear,Уважаемый (ая), +Dear,Уважаемый(ая), Default,По умолчанию, Define coupon codes.,Определить коды купонов., Delayed Days,Задержанные дни, @@ -3836,7 +3836,7 @@ Make Journal Entry,Сделать запись в журнале, Make Purchase Invoice,Сделать счет на покупку, Manufactured,Изготовлено, Mark Work From Home,Пометить работу из дома, -Master,Магистр, +Master,Мастер, Max strength cannot be less than zero.,Максимальная сила не может быть меньше нуля., Maximum attempts for this quiz reached!,Максимальное количество попыток для этого теста достигнуто!, Message,Сообщение, @@ -3899,7 +3899,7 @@ Penalty Amount,Сумма штрафа, Pending,В ожидании, Performance,Производительность, Period based On,Период на основе, -Perpetual inventory required for the company {0} to view this report.,"Постоянная инвентаризация требуется для компании {0}, чтобы просмотреть этот отчет.", +Perpetual inventory required for the company {0} to view this report.,"Чтобы посмотреть этот отчет, требуется постоянная инвентаризация для комнаии {0}", Phone,Телефон, Pick List,Список выбора, Plaid authentication error,Ошибка аутентификации пледа, @@ -3995,25 +3995,25 @@ Review,Обзор, Room,Комната, Room Type,Тип комнаты, Row # ,Строка # , -Row #{0}: Accepted Warehouse and Supplier Warehouse cannot be same,Строка # {0}: принятый склад и склад поставщика не могут быть одинаковыми, -Row #{0}: Cannot delete item {1} which has already been billed.,"Строка # {0}: невозможно удалить элемент {1}, для которого уже выставлен счет.", -Row #{0}: Cannot delete item {1} which has already been delivered,"Строка # {0}: невозможно удалить элемент {1}, который уже был доставлен", -Row #{0}: Cannot delete item {1} which has already been received,"Строка # {0}: невозможно удалить элемент {1}, который уже был получен", -Row #{0}: Cannot delete item {1} which has work order assigned to it.,"Строка # {0}: невозможно удалить элемент {1}, которому назначено рабочее задание.", -Row #{0}: Cannot delete item {1} which is assigned to customer's purchase order.,"Строка # {0}: невозможно удалить элемент {1}, который назначен заказу клиента на покупку.", -Row #{0}: Cannot select Supplier Warehouse while suppling raw materials to subcontractor,Строка № {0}: невозможно выбрать склад поставщика при подаче сырья субподрядчику, -Row #{0}: Cost Center {1} does not belong to company {2},Строка # {0}: МВЗ {1} не принадлежит компании {2}, -Row #{0}: Operation {1} is not completed for {2} qty of finished goods in Work Order {3}. Please update operation status via Job Card {4}.,"Строка # {0}: операция {1} не завершена для {2} количества готовой продукции в рабочем задании {3}. Пожалуйста, обновите статус операции с помощью Job Card {4}.", -Row #{0}: Payment document is required to complete the transaction,Строка # {0}: для завершения транзакции требуется платежный документ, -Row #{0}: Serial No {1} does not belong to Batch {2},Строка # {0}: серийный номер {1} не принадлежит партии {2}, -Row #{0}: Service End Date cannot be before Invoice Posting Date,Строка # {0}: дата окончания обслуживания не может быть раньше даты проводки счета, -Row #{0}: Service Start Date cannot be greater than Service End Date,Строка # {0}: дата начала обслуживания не может быть больше даты окончания обслуживания, -Row #{0}: Service Start and End Date is required for deferred accounting,Строка # {0}: дата начала и окончания обслуживания требуется для отложенного учета, +Row #{0}: Accepted Warehouse and Supplier Warehouse cannot be same,Строка #{0}: склад для получения и склад поставщика не могут быть одинаковыми, +Row #{0}: Cannot delete item {1} which has already been billed.,"Строка #{0}: невозможно удалить продукт {1}, для которого уже выставлен счет.", +Row #{0}: Cannot delete item {1} which has already been delivered,"Строка #{0}: невозможно удалить продукт {1}, который уже был доставлен", +Row #{0}: Cannot delete item {1} which has already been received,"Строка #{0}: невозможно удалить продукт {1}, который уже был получен", +Row #{0}: Cannot delete item {1} which has work order assigned to it.,"Строка #{0}: невозможно удалить продукт {1}, которому назначено рабочее задание.", +Row #{0}: Cannot delete item {1} which is assigned to customer's purchase order.,"Строка #{0}: невозможно удалить продукт {1}, который есть в заказе клиента на покупку.", +Row #{0}: Cannot select Supplier Warehouse while suppling raw materials to subcontractor,Строка #{0}: невозможно выбрать склад поставщика при подаче сырья субподрядчику, +Row #{0}: Cost Center {1} does not belong to company {2},Строка #{0}: МВЗ {1} не принадлежит компании {2}, +Row #{0}: Operation {1} is not completed for {2} qty of finished goods in Work Order {3}. Please update operation status via Job Card {4}.,"Строка #{0}: операция {1} не завершена для {2} количества готовой продукции в рабочем задании {3}. Пожалуйста, обновите статус операции с помощью Карточки работ {4}.", +Row #{0}: Payment document is required to complete the transaction,Строка #{0}: для завершения транзакции требуется платежный документ, +Row #{0}: Serial No {1} does not belong to Batch {2},Строка #{0}: серийный номер {1} не принадлежит партии {2}, +Row #{0}: Service End Date cannot be before Invoice Posting Date,Строка #{0}: дата окончания обслуживания не может быть раньше даты проводки счета, +Row #{0}: Service Start Date cannot be greater than Service End Date,Строка #{0}: дата начала обслуживания не может быть больше даты окончания обслуживания, +Row #{0}: Service Start and End Date is required for deferred accounting,Строка #{0}: дата начала и окончания обслуживания требуется для отложенного учета, Row {0}: Invalid Item Tax Template for item {1},Строка {0}: неверный шаблон налога на товар для товара {1}, Row {0}: Quantity not available for {4} in warehouse {1} at posting time of the entry ({2} {3}),Строка {0}: количество недоступно для {4} на складе {1} во время проводки записи ({2} {3}), Row {0}: user has not applied the rule {1} on the item {2},Строка {0}: пользователь не применил правило {1} к элементу {2}, Row {0}:Sibling Date of Birth cannot be greater than today.,"Строка {0}: дата рождения родного брата не может быть больше, чем сегодня.", -Row({0}): {1} is already discounted in {2},Строка ({0}): {1} уже дисконтирован в {2}, +Row({0}): {1} is already discounted in {2},Строка({0}): {1} уже дисконтирован в {2}, Rows Added in {0},Строки добавлены в {0}, Rows Removed in {0},Строки удалены в {0}, Sanctioned Amount limit crossed for {0} {1},Предел санкционированной суммы для {0} {1}, @@ -4125,7 +4125,7 @@ Unknown Caller,Неизвестный абонент, Unlink external integrations,Отключить внешние интеграции, Unmarked Attendance for days,Посещаемость без опознавательных знаков в течение нескольких дней, Unpublish Item,Отменить публикацию, -Unreconciled,несверенный, +Unreconciled,Несверенный, Unsupported GST Category for E-Way Bill JSON generation,Неподдерживаемая категория НДС для генерации E-Way Bill JSON, Update,Обновить, Update Details,Обновить данные, @@ -4224,7 +4224,7 @@ To date cannot be before From date,На сегодняшний день не м Write Off,Списать, {0} Created,{0} Создано, Email Id,Email ID, -No,№, +No,Нет, Reference Doctype,Ссылка DocType, User Id,ID пользователя, Yes,Да, @@ -4575,8 +4575,8 @@ Get Payment Entries,Получить Записи оплаты, Payment Entries,Записи оплаты, Update Clearance Date,Обновление просвет Дата, Bank Reconciliation Detail,Подробности банковской сверки, -Cheque Number,Чек Количество, -Cheque Date,Чек Дата, +Cheque Number,Номер чека, +Cheque Date,Дата чека, Statement Header Mapping,Сопоставление заголовков операторов, Statement Headers,Заголовки операторов, Transaction Data Mapping,Сопоставление данных транзакций, @@ -4660,7 +4660,7 @@ POS-CLO-,POS-CLO-, Custody,Опека, Net Amount,Чистая сумма, Cashier Closing Payments,Кассовые платежи, -Chart of Accounts Importer,План счетов импортера, +Chart of Accounts Importer,Импорт плана счетов, Import Chart of Accounts from a csv file,Импортировать план счетов из CSV-файла, Attach custom Chart of Accounts file,Прикрепить пользовательский файл плана счетов, Chart Preview,Предварительный просмотр диаграммы, @@ -4668,12 +4668,12 @@ Chart Tree,Дерево Диаграммы, Cheque Print Template,Чеками печати шаблона, Has Print Format,Имеет формат печати, Primary Settings,Основные настройки, -Cheque Size,Cheque Размер, +Cheque Size,Размер чека, Regular,Обычный, Starting position from top edge,Исходное положение от верхнего края, -Cheque Width,Cheque Ширина, -Cheque Height,Cheque Высота, -Scanned Cheque,Сканированные чеками, +Cheque Width,Ширина чека, +Cheque Height,Высота чека, +Scanned Cheque,Отсканированный чек, Is Account Payable,Является ли кредиторская задолженность, Distance from top edge,Расстояние от верхнего края, Distance from left edge,Расстояние от левого края, @@ -4693,9 +4693,9 @@ lft,лев, rgt,прав, Coupon Code,Код купона, Coupon Name,Название купона, -"e.g. ""Summer Holiday 2019 Offer 20""","например, "Летние каникулы 2019 Предложение 20"", +"e.g. ""Summer Holiday 2019 Offer 20""","например, ""Летние каникулы 2019 Предложение 20""", Coupon Type,Тип купона, -Promotional,рекламный, +Promotional,Рекламный, Gift Card,Подарочная карта, unique e.g. SAVE20 To be used to get discount,"уникальный, например, SAVE20 для получения скидки", Validity and Usage,Срок действия и использование, @@ -4760,7 +4760,7 @@ Excise Entry,Акцизный запись, Write Off Entry,Списание запись, Opening Entry,Начальная запись, ACC-JV-.YYYY.-,ACC-JV-.YYYY.-, -Accounting Entries,Бухгалтерские Проводки, +Accounting Entries,Бухгалтерские проводки, Total Debit,Общий дебет, Total Credit,Общий кредит, Difference (Dr - Cr),Разница (Деб - Кред), @@ -4811,7 +4811,7 @@ Loyalty Program Help,Помощь в программе лояльности, Loyalty Program Collection,Коллекция программы лояльности, Tier Name,Название уровня, Minimum Total Spent,Минимальные общие затраты, -Collection Factor (=1 LP),Коэффициент сбора (= 1 Балл), +Collection Factor (=1 LP),Коэффициент сбора (=1 Балл), For how much spent = 1 Loyalty Point,За сколько потраченных = 1 Балл лояльности, Mode of Payment Account,Форма оплаты счета, Default Account,По умолчанию учетная запись, @@ -4840,14 +4840,14 @@ Account Paid From,Счет Оплачено из, Account Paid To,Счет оплачены до, Paid Amount (Company Currency),Оплаченная сумма (в валюте компании), Received Amount,Полученная сумма, -Received Amount (Company Currency),Полученная сумма (валюта компании), +Received Amount (Company Currency),Полученная сумма (в валюте компании), Get Outstanding Invoice,Получить выдающийся счет, Payment References,Ссылки оплаты, Writeoff,Списать, -Total Allocated Amount,Общая сумма Обозначенная, -Total Allocated Amount (Company Currency),Общая Выделенная сумма (валюта компании), +Total Allocated Amount,Общая выделенная сумма, +Total Allocated Amount (Company Currency),Общая выделенная сумма (в валюте компании), Set Exchange Gain / Loss,Установить Курсовая прибыль / убыток, -Difference Amount (Company Currency),Разница Сумма (валюта компании), +Difference Amount (Company Currency),Разница (в валюте компании), Write Off Difference Amount,Списание разница в, Deductions or Loss,Отчисления или убыток, Payment Deductions or Loss,Отчисления оплаты или убыток, @@ -5002,35 +5002,35 @@ Raw Materials Supplied,Поставка сырья, Supplier Warehouse,Склад поставщика, Pricing Rules,Правила ценообразования, Supplied Items,Поставляемые продукты, -Total (Company Currency),Всего (валюта компании), -Net Total (Company Currency),Чистая Всего (валюта компании), +Total (Company Currency),Всего (в валюте компании), +Net Total (Company Currency),Чистая Всего (в валюте компании), Total Net Weight,Общий вес нетто, Shipping Rule,Правило доставки, Purchase Taxes and Charges Template,Купить налоги и сборы шаблон, Purchase Taxes and Charges,Покупка Налоги и сборы, Tax Breakup,Распределение налогов, Taxes and Charges Calculation,Налоги и сборы Расчет, -Taxes and Charges Added (Company Currency),Налоги и сборы Добавил (валюта компании), -Taxes and Charges Deducted (Company Currency),"Налоги, которые вычитаются (валюта компании)", -Total Taxes and Charges (Company Currency),Всего Налоги и сборы (валюта компании), +Taxes and Charges Added (Company Currency),Добавленные налоги и сборы (в валюте компании), +Taxes and Charges Deducted (Company Currency),"Налоги, которые вычитаются (в валюте компании)", +Total Taxes and Charges (Company Currency),Всего налогов и сборов (в валюте компании), Taxes and Charges Added,Налоги и сборы добавлены, Taxes and Charges Deducted,"Налоги и сборы, вычитаемые", Total Taxes and Charges,Общие налоги и сборы, Additional Discount,Дополнительная скидка, Apply Additional Discount On,Применить дополнительную скидку на, -Additional Discount Amount (Company Currency),Сумма дополнительных скидок (валюта компании), +Additional Discount Amount (Company Currency),Сумма дополнительных скидок (в валюте компании), Additional Discount Percentage,Дополнительная скидка в процентах, Additional Discount Amount,Сумма дополнительной скидки, -Grand Total (Company Currency),Общий итог (валюта компании), -Rounding Adjustment (Company Currency),Коррекция округления (валюта компании), -Rounded Total (Company Currency),Округлые Всего (валюта компании), -In Words (Company Currency),Словами (валюта компании), +Grand Total (Company Currency),Общий итог (в валюте компании), +Rounding Adjustment (Company Currency),Коррекция округления (в валюте компании), +Rounded Total (Company Currency),Всего округленно (в валюте компании), +In Words (Company Currency),Словами (в валюте компании), Rounding Adjustment,Коррекция округления, In Words,Прописью, Total Advance,Общий аванс, Disable Rounded Total,Отключение закругленными Итого, Cash/Bank Account,Наличные / Банковский счет, -Write Off Amount (Company Currency),Сумма списаний (валюта компании), +Write Off Amount (Company Currency),Сумма списаний (в валюте компании), Set Advances and Allocate (FIFO),Установите авансы и распределите (FIFO), Get Advances Paid,Получить авансы выданные, Advances,Авансы, @@ -5055,14 +5055,14 @@ Accepted Qty,Принятое кол-во, Rejected Qty,Отклоненое кол-во, UOM Conversion Factor,Коэффициент пересчета единицы измерения, Discount on Price List Rate (%),Скидка от прайс-листа (%), -Price List Rate (Company Currency),Прайс-лист Тариф (валюта компании), +Price List Rate (Company Currency),Прайс-лист Тариф (в валюте компании), Rate ,Цена , -Rate (Company Currency),Тариф (валюта компании), -Amount (Company Currency),Сумма (валюта компании), +Rate (Company Currency),Тариф (в валюте компании), +Amount (Company Currency),Сумма (в валюте компании), Is Free Item,Это бесплатный товар, Net Rate,Нетто-ставка, -Net Rate (Company Currency),Чистая стоимость (валюта компании), -Net Amount (Company Currency),Чистая сумма (валюта компании), +Net Rate (Company Currency),Чистая стоимость (в валюте компании), +Net Amount (Company Currency),Чистая сумма (в валюте компании), Item Tax Amount Included in Value,"Сумма налога на имущество, включенная в стоимость", Landed Cost Voucher Amount,Земельные стоимости путевки сумма, Raw Materials Supplied Cost,Стоимость поставленного сырья, @@ -5135,9 +5135,9 @@ Redemption Cost Center,Центр выкупа, In Words will be visible once you save the Sales Invoice.,В записях будет видно как только вы сохраните счет продажи., Allocate Advances Automatically (FIFO),Автоматическое выделение авансов (FIFO), Get Advances Received,Получить авансы полученные, -Base Change Amount (Company Currency),Базовая Изменение Сумма (Компания Валюта), +Base Change Amount (Company Currency),Базовая Изменение Сумма (в валюте компании), Write Off Outstanding Amount,Списание суммы задолженности, -Terms and Conditions Details,Условия Подробности, +Terms and Conditions Details,Дополнительные условия, Is Internal Customer,Внутренний клиент, Is Discounted,Со скидкой, Unpaid and Discounted,Неоплачиваемый и со скидкой, @@ -5158,7 +5158,7 @@ Qty as per Stock UOM,Кол-во в соответствии с ед.измер Discount and Margin,Скидка и маржа, Rate With Margin,Оценить с маржой, Discount (%) on Price List Rate with Margin,Скидка (%) на цену Прейскурант с маржой, -Rate With Margin (Company Currency),Ставка с маржей (валюта компании), +Rate With Margin (Company Currency),Ставка с маржей (в валюте компании), Delivered By Supplier,Доставлено поставщиком, Deferred Revenue,Отложенный доход, Deferred Revenue Account,Отложенный счет доходов, @@ -5168,12 +5168,12 @@ Customer Warehouse (Optional),Склад Клиент (Необязательн Available Batch Qty at Warehouse,Доступное кол-во пакетов на складе, Available Qty at Warehouse,Доступное кол-во на складе, Delivery Note Item,Доставляемый продукт, -Base Amount (Company Currency),Базовая сумма (валюта компании), +Base Amount (Company Currency),Базовая сумма (в валюте компании), Sales Invoice Timesheet,Счет по табелю, Time Sheet,Табель учета рабочего времени, Billing Hours,Оплачеваемые часы, Timesheet Detail,Сведения о расписании, -Tax Amount After Discount Amount (Company Currency),Сумма налога после скидки Сумма (Компания валют), +Tax Amount After Discount Amount (Company Currency),Сумма налога после суммы скидки (в валюте компании), Item Wise Tax Detail,Подробная информация о налоге на товар, Parenttype,ParentType, "Standard tax template that can be applied to all Sales Transactions. This template can contain list of tax heads and also other expense / income heads like ""Shipping"", ""Insurance"", ""Handling"" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on ""Previous Row Total"" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Is this Tax included in Basic Rate?: If you check this, it means that this tax will not be shown below the item table, but will be included in the Basic Rate in your main item table. This is useful where you want give a flat price (inclusive of all taxes) price to customers.","Стандартный шаблон налог, который может быть применен ко всем сделок купли-продажи. Этот шаблон может содержать перечень налоговых руководителей, а также других глав расходы / доходы, как ""Shipping"", ""Insurance"", ""Обращение"" и т.д. \n\n #### Примечание \n\n ставка налога на Вы Определить здесь будет стандартная ставка налога на прибыль для всех ** деталей **. Если есть ** товары **, которые имеют различные цены, они должны быть добавлены в ** деталь налога ** стол в ** деталь ** мастера.\n\n #### Описание колонок \n\n 1. Расчет Тип: \n - Это может быть ** Чистый Всего ** (то есть сумма основной суммы).\n - ** На предыдущей строке Total / сумма ** (по совокупности налогов и сборов). Если вы выбираете эту опцию, налог будет применяться в процентах от предыдущего ряда (в налоговом таблицы) суммы или объема.\n - ** ** Фактический (как уже упоминалось).\n 2. Счет Руководитель: лицевому счету, при которых этот налог будут забронированы \n 3. Центр Стоимость: Если налог / налог на заряд доход (как перевозка груза) или расходов это должен быть забронирован на МВЗ.\n 4. Описание: Описание налога (которые будут напечатаны в счетах-фактурах / кавычек).\n 5. Оценить: Налоговая ставка.\n 6. Количество: Сумма налога.\n 7. Всего: Суммарное к этой точке.\n 8. Введите Row: Если на базе ""Предыдущая сумма по строке"" вы можете выбрать номер строки которой будет приниматься в качестве основы для такого расчета (по умолчанию предыдущего ряда).\n 9. Это налог Включено в основной ставке ?: Если вы посмотрите, это значит, что этот налог не будет показано ниже в таблице элементов, но будет включен в основной ставке в основной таблице элементов. Это полезно, если вы хотите дать квартира Цена (включая все налоги) цену к клиентам.", @@ -5241,7 +5241,7 @@ Grace Period,Льготный период, Number of days after invoice date has elapsed before canceling subscription or marking subscription as unpaid,Количество дней после истечения срока выставления счета перед отменой подписки или подпиской по подписке как неоплаченной, Prorate,пропорциональная доля, Tax Rule,Налоговое положение, -Tax Type,Налоги Тип, +Tax Type,Тип налога, Use for Shopping Cart,Используйте корзину для, Billing City,Город платильщика, Billing County,Платежный County, @@ -5264,7 +5264,7 @@ Linked Doctype,Связанный Doctype, Water Analysis,Анализ воды, Soil Analysis,Анализ почвы, Plant Analysis,Анализ растений, -Fertilizer,удобрение, +Fertilizer,Удобрение, Soil Texture,Текстура почвы, Weather,Погода, Agriculture Manager,Менеджер по развитию, @@ -5527,7 +5527,7 @@ Supplier Type,Тип поставщика, Allow Purchase Invoice Creation Without Purchase Order,Разрешить создание счета без заказа на покупку, Allow Purchase Invoice Creation Without Purchase Receipt,Разрешить создание счета без квитанции о покупке, Warn RFQs,Предупреждать о RFQ, -Warn POs,Предупредить ПО, +Warn POs,Предупреждать ПО, Prevent RFQs,Предотвращение запросов, Prevent POs,Предотвращение PO, Billing Currency,Платежная валюта, @@ -5543,9 +5543,9 @@ Statutory info and other general information about your Supplier,Уставны PUR-SQTN-.YYYY.-,PUR-SQTN-.YYYY.-, Supplier Address,Адрес поставщика, Link to material requests,Ссылка на заявки на материалы, -Rounding Adjustment (Company Currency,Корректировка округления (Валюта компании, +Rounding Adjustment (Company Currency,Корректировка округления (в валюте компании, Auto Repeat Section,Секция автоматического повтора, -Is Subcontracted,Является субподряду, +Is Subcontracted,Является субподрядом, Lead Time in days,Время выполнения в днях, Supplier Score,Оценка поставщика, Indicator Color,Цвет индикатора, @@ -5605,13 +5605,13 @@ Ringing,Звонок, Missed,Пропущенный, Call Duration in seconds,Продолжительность звонка в секундах, Recording URL,Запись URL, -Communication Medium,Связь Средний, -Communication Medium Type,Тип средств связи, +Communication Medium,Способ коммуникации, +Communication Medium Type,Тип способа коммуникации, Voice,Голос, Catch All,Поймать все, "If there is no assigned timeslot, then communication will be handled by this group","Если нет назначенного временного интервала, то связь будет обрабатываться этой группой", Timeslots,Временные интервалы, -Communication Medium Timeslot,Коммуникационный средний таймслот, +Communication Medium Timeslot,Коммуникационный таймслот, Employee Group,Группа сотрудников, Appointment,"Деловое свидание, встреча", Scheduled Time,Назначенное время, @@ -5638,14 +5638,14 @@ Success Redirect URL,URL-адрес успешного перенаправле "Leave blank for home.\nThis is relative to site URL, for example ""about"" will redirect to ""https://yoursitename.com/about""","Оставьте пустым для дома. Это относительно URL сайта, например, «about» будет перенаправлен на «https://yoursitename.com/about»", Appointment Booking Slots,Назначение Бронирование Слоты, Day Of Week,День недели, -From Time ,С , +From Time ,От времени , Campaign Email Schedule,Расписание рассылки кампании, Send After (days),Отправить после (дней), Signed,подписанный, Party User,Пользователь Party, Unsigned,Неподписанный, Fulfilment Status,Статус выполнения, -N/A,Н/д, +N/A,Н/Д, Unfulfilled,Невыполненный, Partially Fulfilled,Частично выполнено, Fulfilled,Исполненная, @@ -5856,7 +5856,7 @@ Is Featured,Показано, Intro Video,Вступительное видео, Program Course,Программа курса, School House,Общежитие, -Boarding Student,Студент-пансионер, +Boarding Student,Студент-интернат, Check this if the Student is residing at the Institute's Hostel.,"Поставьте галочку, если студент проживает в общежитии института", Walking,Пешком, Institute's Bus,Автобус института, @@ -5891,7 +5891,7 @@ Grading Basis,Оценка основ, Latest Highest Score,Последний наивысший балл, Latest Attempt,Последняя попытка, Quiz Activity,Викторина, -Enrollment,регистрация, +Enrollment,Регистрация, Pass,Проходить, Quiz Question,Контрольный вопрос, Quiz Result,Результат теста, @@ -5936,9 +5936,9 @@ Application Fee,Регистрационный взнос, Naming Series (for Student Applicant),Идентификация по Имени (для заявителей-студентов), LMS Only,Только LMS, EDU-APP-.YYYY.-,EDU-APP-.YYYY.-, -Application Status,Статус приложения, +Application Status,Статус подачи документов, Application Date,Дата подачи документов, -Student Attendance Tool,Student Участники Инструмент, +Student Attendance Tool,Инструмент посещаемости учащихся, Group Based On,Группа на основе, Students HTML,Студенты HTML, Group Based on,Группа основана на, @@ -6050,7 +6050,7 @@ Shopify Settings,Shopify настройки, status html,статус html, Enable Shopify,Включить Shopify, App Type,Тип приложения, -Last Sync Datetime,Последнее время синхронизации, +Last Sync Datetime,Последняя дата синхронизации, Shop URL,URL магазина, eg: frappe.myshopify.com,например: frappe.myshopify.com, Shared secret,Общий секрет, @@ -6092,7 +6092,7 @@ Processed Files,Обработанные файлы, Parties,Стороны, UOMs,Единицы измерения, Vouchers,Ваучеры, -Round Off Account,Округление аккаунт, +Round Off Account,Округлить счет, Day Book Data,Данные Дневной Книги, Day Book Data exported from Tally that consists of all historic transactions,"Данные дневной книги, экспортированные из Tally, которые включают все исторические транзакции", Is Day Book Data Processed,Обработаны ли данные дневника, @@ -7129,11 +7129,11 @@ Loan ,ссуда, Shortfall Time,Время нехватки, America/New_York,Америка / Триатлон, Shortfall Amount,Сумма дефицита, -Security Value ,Значение безопасности, +Security Value ,Значение безопасности , Process Loan Security Shortfall,Недостаток безопасности процесса займа, Loan To Value Ratio,Соотношение займа к стоимости, Unpledge Time,Время невыплаты, -Loan Name,Кредит Имя, +Loan Name,Название кредита, Rate of Interest (%) Yearly,Процентная ставка (%) Годовой, Penalty Interest Rate (%) Per Day,Процентная ставка штрафа (%) в день, Penalty Interest Rate is levied on the pending interest amount on a daily basis in case of delayed repayment ,Штрафная процентная ставка взимается на сумму отложенного процента на ежедневной основе в случае задержки выплаты, @@ -7199,11 +7199,11 @@ Scrap Items,Утилизированные продукты, Operating Cost,Эксплуатационные затраты, Raw Material Cost,Стоимость сырья, Scrap Material Cost,Лом Материал Стоимость, -Operating Cost (Company Currency),Эксплуатационные расходы (Компания Валюта), -Raw Material Cost (Company Currency),Стоимость сырья (валюта компании), -Scrap Material Cost(Company Currency),Скрапа Стоимость (Компания Валюта), +Operating Cost (Company Currency),Эксплуатационные расходы (в валюте компании), +Raw Material Cost (Company Currency),Стоимость сырья (в валюте компании), +Scrap Material Cost(Company Currency),Стоимость отходов (в валюте компании), Total Cost,Общая стоимость, -Total Cost (Company Currency),Общая стоимость (валюта компании), +Total Cost (Company Currency),Общая стоимость (в валюте компании), Materials Required (Exploded),Необходимые материалы (в разобранном), Exploded Items,Взорванные предметы, Show in Website,Показать на веб-сайте, @@ -7219,17 +7219,17 @@ Include Item In Manufacturing,Включить товар в производс BOM Item,Спецификация продукта, Item operation,Работа с элементами, Rate & Amount,Стоимость и сумма, -Basic Rate (Company Currency),Основная ставка (валюта компании), -Scrap %,Лом%, +Basic Rate (Company Currency),Основная ставка (в валюте компании), +Scrap %,Брак %, Original Item,Оригинальный товар, BOM Operation,Операция спецификации, Operation Time ,Время операции, In minutes,В считанные минуты, Batch Size,Размер партии, -Base Hour Rate(Company Currency),Базовый час Rate (Компания Валюта), -Operating Cost(Company Currency),Эксплуатационные расходы (Компания Валюта), +Base Hour Rate(Company Currency),Базовый час Rate (в валюте компании), +Operating Cost(Company Currency),Эксплуатационные расходы (в валюте компании), BOM Scrap Item,Спецификация отходов продукта, -Basic Amount (Company Currency),Базовая сумма (Компания Валюта), +Basic Amount (Company Currency),Базовая сумма (в валюте компании), BOM Update Tool,Инструмент обновления спецификации, "Replace a particular BOM in all other BOMs where it is used. It will replace the old BOM link, update cost and regenerate ""BOM Explosion Item"" table as per new BOM.\nIt also updates latest price in all the BOMs.","Замените конкретную спецификацию во всех других спецификациях, где она используется. Он заменит старую ссылку BOM, обновит стоимость и восстановит таблицу «BOM Explosion Item» в соответствии с новой спецификацией. Он также обновляет последнюю цену во всех спецификациях.", Replace BOM,Заменить спецификацию, @@ -7322,11 +7322,11 @@ This is a location where raw materials are available.,"Это место, где Work-in-Progress Warehouse,Работа-в-Прогресс Склад, This is a location where operations are executed.,"Это место, где выполняются операции.", This is a location where final product stored.,"Это место, где хранится конечный продукт.", -Scrap Warehouse,Лом Склад, +Scrap Warehouse,Склад брака, This is a location where scraped materials are stored.,"Это место, где хранятся скребки.", Required Items,Требуемые товары, -Actual Start Date,Фактическая Дата начала, -Planned End Date,Планируемая Дата завершения, +Actual Start Date,Фактическая дата начала, +Planned End Date,Планируемая дата завершения, Actual End Date,Факт. дата окончания, Operation Cost,Стоимость эксплуатации, Planned Operating Cost,Планируемые Эксплуатационные расходы, @@ -7348,9 +7348,9 @@ in Minutes,Через несколько минут, Actual Time and Cost,Фактическое время и стоимость, Actual Start Time,Фактическое время начала, Actual End Time,Фактическое время окончания, -Updated via 'Time Log',"Обновлено помощью ""Time Вход""", +Updated via 'Time Log',"Обновлено через 'Журнал времени'", Actual Operation Time,Фактическая время работы, -in Minutes\nUpdated via 'Time Log',"в минутах \n Обновлено помощью ""Time Вход""", +in Minutes\nUpdated via 'Time Log',"в минутах \n Обновлено через 'Журнал времени'", (Hour Rate / 60) * Actual Operation Time,(часовая ставка ÷ 60) × фактическое время работы, Workstation Name,Название рабочего места, Production Capacity,Производственная мощность, @@ -7377,7 +7377,7 @@ Certification Validity,Срок действия сертификации, Discuss ID,Обсудить ID, GitHub ID,Идентификатор GitHub, Non Profit Manager,Менеджер некоммерческих организаций, -Chapter Head,Глава главы, +Chapter Head,Заголовок главы, Meetup Embed HTML,Вставить HTML-код, chapters/chapter_name\nleave blank automatically set after saving chapter.,главы / chapter_name оставить пустым автоматически после сохранения главы., Chapter Members,Члены группы, @@ -7435,7 +7435,7 @@ Tag Line,Тег линии, Company Tagline for website homepage,Слоган компании на главной странице сайта, Company Description for website homepage,Описание компании на главной странице сайта, Homepage Slideshow,Слайдшоу на домашней странице, -"URL for ""All Products""",URL для "Все продукты", +"URL for ""All Products""",URL для ""Все продукты""", Products to be shown on website homepage,Продукты будут показаны на главной странице сайта, Homepage Featured Product,Рекомендуемые продукты на главной страницу, route,маршрут, @@ -7514,7 +7514,7 @@ Ignore User Time Overlap,Игнорировать перекрытие поль Ignore Employee Time Overlap,Игнорировать перекрытие времени сотрудников, Weight,Вес, Parent Task,Родительская задача, -Timeline,График, +Timeline,Хронология, Expected Time (in hours),Ожидаемое время (в часах), % Progress,% Прогресс, Is Milestone,Является этапом, @@ -7736,14 +7736,14 @@ Close Opportunity After Days,"Закрыть Выявление Через, дн Default Quotation Validity Days,"Число дней по умолчанию, в течение которых Предложение действительно", Sales Update Frequency,Частота обновления продаж, Each Transaction,Каждая транзакция, -SMS Center,СМС-центр, +SMS Center,SMS-центр, Send To,Отправить, All Contact,Всем контактам, All Customer Contact,Контакты всех клиентов, All Supplier Contact,Всем контактам поставщиков, All Sales Partner Contact,Всем контактам торговых партнеров, -All Lead (Open),Всем лидам (Созданным), -All Employee (Active),Всем сотрудникам (Активным), +All Lead (Open),Всем лидам (Открыт), +All Employee (Active),Всем сотрудникам (Активный), All Sales Person,Всем продавцам, Create Receiver List,Создать список получателей, Receiver List,Список получателей, @@ -7792,7 +7792,7 @@ Exchange Gain / Loss Account,Обмен Прибыль / убытках, Unrealized Exchange Gain/Loss Account,Нереализованная учетная ставка по обмену / убытку, Allow Account Creation Against Child Company,Разрешить создание аккаунта против дочерней компании, Default Payable Account,По умолчанию оплачивается аккаунт, -Default Employee Advance Account,Default Advance Account, +Default Employee Advance Account,Авансовый счет сотрудника по умолчанию, Default Cost of Goods Sold Account,По умолчанию Себестоимость проданных товаров счет, Default Income Account,Счет дохода по умолчанию, Default Deferred Revenue Account,По умолчанию отложенная учетная запись, @@ -8219,7 +8219,7 @@ Price List Master,Прайс-лист Мастер, Price List Name,Название прайс-листа, Price Not UOM Dependent,Цена не зависит от UOM, Applicable for Countries,Применимо для стран, -Price List Country,Цены Страна, +Price List Country,Прайс лист страны, MAT-PRE-.YYYY.-,MAT-PRE-.YYYY.-, Supplier Delivery Note,Доставочный лист, Time at which materials were received,Время получения материалов, @@ -8327,7 +8327,7 @@ Outgoing Rate,Исходящие Оценить, Actual Qty After Transaction,Остаток после проведения, Stock Value Difference,Расхождение стоимости запасов, Stock Queue (FIFO),Фото со Очередь (FIFO), -Is Cancelled,Является Отмененные, +Is Cancelled,Является отмененным, Stock Reconciliation,Инвентаризация запасов, This tool helps you to update or fix the quantity and valuation of stock in the system. It is typically used to synchronise the system values and what actually exists in your warehouses.,"Этот инструмент поможет вам обновить или исправить количество и оценку запасов в системе. Это, как правило, используется для синхронизации системных значений и то, что на самом деле существует в ваших складах.", MAT-RECO-.YYYY.-,MAT-RECO-.YYYY.-, @@ -8505,15 +8505,15 @@ IRS 1099,IRS 1099, Issued Items Against Work Order,Продукты выпущенные под заказ, Projected Quantity as Source,Планируемое количество как источник, Item Balance (Simple),Остаток продукта (простой), -Item Price Stock,Цена товара, +Item Price Stock,Стоимость продукта на складе, Item Prices,Цены продукта, Item Shortage Report,Отчет о нехватке продуктов, Item Variant Details,Подробности модификации продукта, Item-wise Price List Rate,Цена продукта в прайс-листе, -Item-wise Purchase History,Пункт мудрый История покупок, -Item-wise Purchase Register,Пункт мудрый Покупка Зарегистрироваться, -Item-wise Sales History,История продаж продуктов, -Item-wise Sales Register,Пункт мудрый Продажи Зарегистрироваться, +Item-wise Purchase History,История покупок по продуктам, +Item-wise Purchase Register,Реестр покупок по продуктам, +Item-wise Sales History,История продаж по продуктам, +Item-wise Sales Register,Реестр продаж по продуктам, Items To Be Requested,Запрашиваемые продукты, Reserved,Зарезервировано, Itemwise Recommended Reorder Level,Рекомендация пополнения уровня продукта, @@ -9045,7 +9045,7 @@ Send Membership Acknowledgement,Отправить подтверждение ч Send Invoice with Email,Отправить счет по электронной почте, Membership Print Format,Формат печати членства, Invoice Print Format,Формат печати счета, -Revoke ,Отозвать<Key></Key>, +Revoke ,Отозвать , You can learn more about memberships in the manual. ,Вы можете узнать больше о членстве в руководстве., ERPNext Docs,ERPСледующие документы, Regenerate Webhook Secret,Восстановить секрет веб-перехватчика, @@ -9080,7 +9080,7 @@ Additional Salary ,Дополнительная зарплата , Unmarked days,Неотмеченные дни, Absent Days,Отсутствующие дни, Conditions and Formula variable and example,"Условия и формула, переменная и пример", -Feedback By,Отзыв Автор, +Feedback By,Отзыв от, Manufacturing Section,Производственный отдел, "By default, the Customer Name is set as per the Full Name entered. If you want Customers to be named by a ","По умолчанию имя клиента устанавливается в соответствии с введенным полным именем. Если вы хотите, чтобы имена клиентов", Configure the default Price List when creating a new Sales transaction. Item prices will be fetched from this Price List.,Настройте прайс-лист по умолчанию при создании новой транзакции продаж. Цены на товары будут взяты из этого прейскуранта., @@ -9322,7 +9322,7 @@ End Date must not be lesser than Start Date,Дата окончания не д Employee {0} already has Active Shift {1}: {2},Сотрудник {0} уже имеет активную смену {1}: {2}, from {0},от {0}, to {0},в {0}, -Please select Employee first.,"Пожалуйста, сначала выберите Сотрудник.", +Please select Employee first.,"Пожалуйста, сначала выберите сотрудника.", Please set {0} for the Employee or for Department: {1},Установите {0} для сотрудника или отдела: {1}, To Date should be greater than From Date,"Дата до должна быть больше, чем Дата", Employee Onboarding: {0} is already for Job Applicant: {1},Прием на работу сотрудника: {0} уже для соискателя: {1}, @@ -9335,7 +9335,7 @@ Asset Value Analytics,Аналитика стоимости активов, Category-wise Asset Value,Стоимость актива по категориям, Total Assets,Итого активы, New Assets (This Year),Новые активы (в этом году), -Row #{}: Depreciation Posting Date should not be equal to Available for Use Date.,Строка № {}: Дата проводки амортизации не должна совпадать с датой доступности для использования., +Row #{}: Depreciation Posting Date should not be equal to Available for Use Date.,Строка №{}: Дата проводки амортизации не должна совпадать с датой доступности для использования., Incorrect Date,Неправильная дата, Invalid Gross Purchase Amount,Неверная сумма покупки брутто, There are active maintenance or repairs against the asset. You must complete all of them before cancelling the asset.,"Активно проводится техническое обслуживание или ремонт актива. Вы должны выполнить их все, прежде чем аннулировать актив.", @@ -9383,7 +9383,7 @@ You can only select one mode of payment as default,По умолчанию вы Missing Account,Отсутствует аккаунт, Customers not selected.,Клиенты не выбраны., Statement of Accounts,Выписка со счетов, -Ageing Report Based On ,Отчет о старении на основе, +Ageing Report Based On ,Отчет о старении на основе , Please enter distributed cost center,"Пожалуйста, введите распределенное МВЗ", Total percentage allocation for distributed cost center should be equal to 100,Общее процентное распределение для распределенного МВЗ должно быть равно 100., Cannot enable Distributed Cost Center for a Cost Center already allocated in another Distributed Cost Center,"Невозможно включить центр распределенных затрат для центра затрат, уже выделенного в другом центре распределенных затрат", @@ -9489,7 +9489,7 @@ Mandatory Results,Обязательные результаты, Sales Invoice or Patient Encounter is required to create Lab Tests,Счет продажи или встреча с пациентом необходимы для создания лабораторных тестов, Insufficient Data,Недостаточные данные, Lab Test(s) {0} created successfully,Лабораторные тесты {0} успешно созданы, -Test :,Контрольная работа :, +Test :,Тест :, Sample Collection {0} has been created,Коллекция образцов {0} создана, Normal Range: ,Нормальный диапазон:, Row #{0}: Check Out datetime cannot be less than Check In datetime,Строка № {0}: Дата и время выезда не может быть меньше даты и времени выезда., @@ -9504,7 +9504,7 @@ Invalid Quantity,Неверное количество, {0} on {1},{0} в {1}, {0} with {1},{0} с {1}, Appointment Confirmation Message Not Sent,Сообщение с подтверждением встречи не отправлено, -"SMS not sent, please check SMS Settings","СМС не отправлено, проверьте настройки СМС", +"SMS not sent, please check SMS Settings","SMS не отправлено, проверьте настройки SMS", Healthcare Service Unit Type cannot have both {0} and {1},Тип единицы медицинского обслуживания не может содержать одновременно {0} и {1}, Healthcare Service Unit Type must allow atleast one among {0} and {1},Тип единицы медицинского обслуживания должен допускать хотя бы одно из {0} и {1}, Set Response Time and Resolution Time for Priority {0} in row {1}.,Задайте время ответа и время разрешения для приоритета {0} в строке {1}., From 2bc6d4607001ca81d4045bb32fbf400231316753 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 26 May 2022 13:00:34 +0530 Subject: [PATCH 075/192] fix: validation message --- erpnext/hr/doctype/job_opening/job_opening.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/job_opening/job_opening.py b/erpnext/hr/doctype/job_opening/job_opening.py index f8e0c7d385..ce7caa33c6 100644 --- a/erpnext/hr/doctype/job_opening/job_opening.py +++ b/erpnext/hr/doctype/job_opening/job_opening.py @@ -55,7 +55,7 @@ class JobOpening(WebsiteGenerator): if number_of_positions <= current_count: frappe.throw( _( - "Job Openings for the designation {0} are already open or the hiring is completed as per the Staffing Plan {1}" + "Job Openings for the designation {0} are already open or the hiring is complete as per the Staffing Plan {1}" ).format( frappe.bold(self.designation), get_link_to_form("Staffing Plan", self.staffing_plan) ), From 82cd54b40b32f4fece4dd9f2ee709a5c29876cd2 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 26 May 2022 12:43:22 +0530 Subject: [PATCH 076/192] chore: resave naming series doctype schema separate commit to avoid mixing actual changes --- .../doctype/naming_series/naming_series.json | 431 ++++-------------- .../naming_series/test_naming_series.py | 9 + 2 files changed, 98 insertions(+), 342 deletions(-) create mode 100644 erpnext/setup/doctype/naming_series/test_naming_series.py diff --git a/erpnext/setup/doctype/naming_series/naming_series.json b/erpnext/setup/doctype/naming_series/naming_series.json index f936dcf3c9..f0d9ece992 100644 --- a/erpnext/setup/doctype/naming_series/naming_series.json +++ b/erpnext/setup/doctype/naming_series/naming_series.json @@ -1,360 +1,107 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2013-01-25 11:35:08", - "custom": 0, - "description": "Set prefix for numbering series on your transactions", - "docstatus": 0, - "doctype": "DocType", - "editable_grid": 0, + "actions": [], + "creation": "2022-05-26 03:12:49.087648", + "description": "Set prefix for numbering series on your transactions", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "setup_series", + "select_doc_for_series", + "help_html", + "set_options", + "user_must_always_select", + "update", + "update_series", + "prefix", + "current_value", + "update_series_start" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Set prefix for numbering series on your transactions", - "fieldname": "setup_series", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Setup Series", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "description": "Set prefix for numbering series on your transactions", + "fieldname": "setup_series", + "fieldtype": "Section Break", + "label": "Setup Series" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "select_doc_for_series", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Select Transaction", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "select_doc_for_series", + "fieldtype": "Select", + "label": "Select Transaction" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "select_doc_for_series", - "fieldname": "help_html", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Help HTML", - "length": 0, - "no_copy": 0, - "options": "
\nEdit list of Series in the box below. Rules:\n
    \n
  • Each Series Prefix on a new line.
  • \n
  • Allowed special characters are \"/\" and \"-\"
  • \n
  • Optionally, set the number of digits in the series using dot (.) followed by hashes (#). For example, \".####\" means that the series will have four digits. Default is five digits.
  • \n
\nExamples:
\nINV-
\nINV-10-
\nINVK-
\nINV-.####
\n
", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "depends_on": "select_doc_for_series", + "fieldname": "help_html", + "fieldtype": "HTML", + "label": "Help HTML", + "options": "
\nEdit list of Series in the box below. Rules:\n
    \n
  • Each Series Prefix on a new line.
  • \n
  • Allowed special characters are \"/\" and \"-\"
  • \n
  • Optionally, set the number of digits in the series using dot (.) followed by hashes (#). For example, \".####\" means that the series will have four digits. Default is five digits.
  • \n
\nExamples:
\nINV-
\nINV-10-
\nINVK-
\nINV-.####
\n
" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "select_doc_for_series", - "fieldname": "set_options", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Series List for this Transaction", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "depends_on": "select_doc_for_series", + "fieldname": "set_options", + "fieldtype": "Text", + "label": "Series List for this Transaction" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "select_doc_for_series", - "description": "Check this if you want to force the user to select a series before saving. There will be no default if you check this.", - "fieldname": "user_must_always_select", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "User must always select", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "0", + "depends_on": "select_doc_for_series", + "description": "Check this if you want to force the user to select a series before saving. There will be no default if you check this.", + "fieldname": "user_must_always_select", + "fieldtype": "Check", + "label": "User must always select" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "select_doc_for_series", - "fieldname": "update", - "fieldtype": "Button", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Update", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "depends_on": "select_doc_for_series", + "fieldname": "update", + "fieldtype": "Button", + "label": "Update" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Change the starting / current sequence number of an existing series.", - "fieldname": "update_series", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Update Series", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "description": "Change the starting / current sequence number of an existing series.", + "fieldname": "update_series", + "fieldtype": "Section Break", + "label": "Update Series" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "prefix", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Prefix", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "prefix", + "fieldtype": "Select", + "label": "Prefix" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "This is the number of the last created transaction with this prefix", - "fieldname": "current_value", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Current Value", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "description": "This is the number of the last created transaction with this prefix", + "fieldname": "current_value", + "fieldtype": "Int", + "label": "Current Value" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "update_series_start", - "fieldtype": "Button", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Update Series Number", - "length": 0, - "no_copy": 0, - "options": "update_series_start", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "update_series_start", + "fieldtype": "Button", + "label": "Update Series Number", + "options": "update_series_start" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 1, - "icon": "fa fa-sort-by-order", - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2017-08-17 03:41:37.685910", - "modified_by": "Administrator", - "module": "Setup", - "name": "Naming Series", - "owner": "Administrator", + ], + "hide_toolbar": 1, + "icon": "fa fa-sort-by-order", + "idx": 1, + "issingle": 1, + "links": [], + "modified": "2022-05-26 03:13:05.357751", + "modified_by": "Administrator", + "module": "Setup", + "name": "Naming Series", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 0, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 1, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "track_changes": 0, - "track_seen": 0 + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/setup/doctype/naming_series/test_naming_series.py b/erpnext/setup/doctype/naming_series/test_naming_series.py new file mode 100644 index 0000000000..51b0e841b5 --- /dev/null +++ b/erpnext/setup/doctype/naming_series/test_naming_series.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestNamingSeries(FrappeTestCase): + pass From 24d1bf5328ce3695c4ff01a7276574c36583dbae Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 26 May 2022 13:13:49 +0530 Subject: [PATCH 077/192] feat: preview next numbers on naming series tool --- .../doctype/naming_series/naming_series.js | 32 ++++++++++++++++++- .../doctype/naming_series/naming_series.json | 29 +++++++++++++++-- .../doctype/naming_series/naming_series.py | 31 +++++++++++++++++- 3 files changed, 88 insertions(+), 4 deletions(-) diff --git a/erpnext/setup/doctype/naming_series/naming_series.js b/erpnext/setup/doctype/naming_series/naming_series.js index 861b2b3983..0fb72abba6 100644 --- a/erpnext/setup/doctype/naming_series/naming_series.js +++ b/erpnext/setup/doctype/naming_series/naming_series.js @@ -54,5 +54,35 @@ frappe.ui.form.on("Naming Series", { frm.events.get_doc_and_prefix(frm); } }); - } + }, + + naming_series_to_check(frm) { + frappe.call({ + method: "preview_series", + doc: frm.doc, + callback: function(r) { + if (!r.exc) { + frm.set_value("preview", r.message); + } else { + frm.set_value("preview", __("Failed to generate preview of series")); + } + } + }); + }, + + add_series(frm) { + const series = frm.doc.naming_series_to_check; + + if (!series) { + frappe.show_alert(__("Please type a valid series.")); + return; + } + + if (!frm.doc.set_options.includes(series)) { + const current_series = frm.doc.set_options; + frm.set_value("set_options", `${current_series}\n${series}`); + } else { + frappe.show_alert(__("Series already added to transaction.")); + } + }, }); diff --git a/erpnext/setup/doctype/naming_series/naming_series.json b/erpnext/setup/doctype/naming_series/naming_series.json index f0d9ece992..7ccde3e396 100644 --- a/erpnext/setup/doctype/naming_series/naming_series.json +++ b/erpnext/setup/doctype/naming_series/naming_series.json @@ -8,9 +8,13 @@ "setup_series", "select_doc_for_series", "help_html", + "naming_series_to_check", + "preview", + "add_series", "set_options", "user_must_always_select", "update", + "column_break_13", "update_series", "prefix", "current_value", @@ -33,7 +37,7 @@ "fieldname": "help_html", "fieldtype": "HTML", "label": "Help HTML", - "options": "
\nEdit list of Series in the box below. Rules:\n
    \n
  • Each Series Prefix on a new line.
  • \n
  • Allowed special characters are \"/\" and \"-\"
  • \n
  • Optionally, set the number of digits in the series using dot (.) followed by hashes (#). For example, \".####\" means that the series will have four digits. Default is five digits.
  • \n
\nExamples:
\nINV-
\nINV-10-
\nINVK-
\nINV-.####
\n
" + "options": "
\nEdit list of Series in the box below. Rules:\n
    \n
  • Each Series Prefix on a new line.
  • \n
  • Allowed special characters are \"/\" and \"-\"
  • \n
  • Optionally, set the number of digits in the series using dot (.) followed by hashes (#). For example, \".####\" means that the series will have four digits. Default is five digits.
  • \n
\nExamples:
\nINV-
\nINV-10-
\nINVK-
\nINV-.####
\n
\n
" }, { "depends_on": "select_doc_for_series", @@ -77,6 +81,27 @@ "fieldtype": "Button", "label": "Update Series Number", "options": "update_series_start" + }, + { + "fieldname": "naming_series_to_check", + "fieldtype": "Data", + "label": "Try a naming Series" + }, + { + "default": " ", + "fieldname": "preview", + "fieldtype": "Text", + "label": "Preview of generated names", + "read_only": 1 + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break" + }, + { + "fieldname": "add_series", + "fieldtype": "Button", + "label": "Add this Series" } ], "hide_toolbar": 1, @@ -84,7 +109,7 @@ "idx": 1, "issingle": 1, "links": [], - "modified": "2022-05-26 03:13:05.357751", + "modified": "2022-05-26 05:19:10.392657", "modified_by": "Administrator", "module": "Setup", "name": "Naming Series", diff --git a/erpnext/setup/doctype/naming_series/naming_series.py b/erpnext/setup/doctype/naming_series/naming_series.py index 4fba776cb5..eafc264f30 100644 --- a/erpnext/setup/doctype/naming_series/naming_series.py +++ b/erpnext/setup/doctype/naming_series/naming_series.py @@ -6,7 +6,7 @@ import frappe from frappe import _, msgprint, throw from frappe.core.doctype.doctype.doctype import validate_series from frappe.model.document import Document -from frappe.model.naming import parse_naming_series +from frappe.model.naming import make_autoname, parse_naming_series from frappe.permissions import get_doctypes_with_read from frappe.utils import cint, cstr @@ -206,6 +206,35 @@ class NamingSeries(Document): prefix = parse_naming_series(parts) return prefix + @frappe.whitelist() + def preview_series(self) -> str: + """Preview what the naming series will generate.""" + + generated_names = [] + series = self.naming_series_to_check + if not series: + return "" + + try: + doc = self._fetch_last_doc_if_available() + for _count in range(3): + generated_names.append(make_autoname(series, doc=doc)) + except Exception as e: + if frappe.message_log: + frappe.message_log.pop() + return _("Failed to generate names from the series") + f"\n{str(e)}" + + # Explcitly rollback in case any changes were made to series table. + frappe.db.rollback() # nosemgrep + return "\n".join(generated_names) + + def _fetch_last_doc_if_available(self): + """Fetch last doc for evaluating naming series with fields.""" + try: + return frappe.get_last_doc(self.select_doc_for_series) + except Exception: + return None + def set_by_naming_series( doctype, fieldname, naming_series, hide_name_field=True, make_mandatory=1 From 4d0e2aa33ae2f1ed7487632a65d1b3741d664b15 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 26 May 2022 15:36:48 +0530 Subject: [PATCH 078/192] docs: update help information on naming series --- erpnext/setup/doctype/naming_series/naming_series.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/naming_series/naming_series.json b/erpnext/setup/doctype/naming_series/naming_series.json index 7ccde3e396..c65a6f0ae4 100644 --- a/erpnext/setup/doctype/naming_series/naming_series.json +++ b/erpnext/setup/doctype/naming_series/naming_series.json @@ -37,7 +37,7 @@ "fieldname": "help_html", "fieldtype": "HTML", "label": "Help HTML", - "options": "
\nEdit list of Series in the box below. Rules:\n
    \n
  • Each Series Prefix on a new line.
  • \n
  • Allowed special characters are \"/\" and \"-\"
  • \n
  • Optionally, set the number of digits in the series using dot (.) followed by hashes (#). For example, \".####\" means that the series will have four digits. Default is five digits.
  • \n
\nExamples:
\nINV-
\nINV-10-
\nINVK-
\nINV-.####
\n
\n
" + "options": "
\n Edit list of Series in the box below. Rules:\n
    \n
  • Each Series Prefix on a new line.
  • \n
  • Allowed special characters are \"/\" and \"-\"
  • \n
  • \n Optionally, set the number of digits in the series using dot (.)\n followed by hashes (#). For example, \".####\" means that the series\n will have four digits. Default is five digits.\n
  • \n
  • \n You can also use variables in the series name by putting them\n between (.) dots\n
    \n Support Variables:\n
      \n
    • .YYYY. - Year in 4 digits
    • \n
    • .YY. - Year in 2 digits
    • \n
    • .MM. - Month
    • \n
    • .DD. - Day of month
    • \n
    • .WW. - Week of the year
    • \n
    • .FY. - Fiscal Year
    • \n
    • \n .{fieldname}. - fieldname on the document e.g.\n branch\n
    • \n
    \n
  • \n
\n Examples:\n
    \n
  • INV-
  • \n
  • INV-10-
  • \n
  • INVK-
  • \n
  • INV-.YYYY.-.{branch}.-.MM.-.####
  • \n
\n
\n
\n" }, { "depends_on": "select_doc_for_series", @@ -109,7 +109,7 @@ "idx": 1, "issingle": 1, "links": [], - "modified": "2022-05-26 05:19:10.392657", + "modified": "2022-05-26 06:06:42.109504", "modified_by": "Administrator", "module": "Setup", "name": "Naming Series", From 964b4184a6ee570d8c4354b42cc3cef133df2c54 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 26 May 2022 16:17:56 +0530 Subject: [PATCH 079/192] test: add basic tests for naming series tool --- .../naming_series/test_naming_series.py | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/naming_series/test_naming_series.py b/erpnext/setup/doctype/naming_series/test_naming_series.py index 51b0e841b5..fce663e4c5 100644 --- a/erpnext/setup/doctype/naming_series/test_naming_series.py +++ b/erpnext/setup/doctype/naming_series/test_naming_series.py @@ -1,9 +1,35 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe +import frappe from frappe.tests.utils import FrappeTestCase +from erpnext.setup.doctype.naming_series.naming_series import NamingSeries + class TestNamingSeries(FrappeTestCase): - pass + def setUp(self): + self.ns: NamingSeries = frappe.get_doc("Naming Series") + + def tearDown(self): + frappe.db.rollback() + + def test_naming_preview(self): + self.ns.select_doc_for_series = "Sales Invoice" + + self.ns.naming_series_to_check = "AXBZ.####" + serieses = self.ns.preview_series().split("\n") + self.assertEqual(["AXBZ0001", "AXBZ0002", "AXBZ0003"], serieses) + + self.ns.naming_series_to_check = "AXBZ-.{currency}.-" + serieses = self.ns.preview_series().split("\n") + + def test_get_transactions(self): + + naming_info = self.ns.get_transactions() + self.assertIn("Sales Invoice", naming_info["transactions"]) + + existing_naming_series = frappe.get_meta("Sales Invoice").get_field("naming_series").options + + for series in existing_naming_series.split("\n"): + self.assertIn(series, naming_info["prefixes"]) From 47b539e638e621e07931a18e4cd5af53fdbba4a4 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 26 May 2022 16:20:56 +0530 Subject: [PATCH 080/192] fix: skip existing batch number during autogen (#31140) --- erpnext/stock/doctype/batch/batch.py | 33 ++++++++++++++--------- erpnext/stock/doctype/batch/test_batch.py | 26 ++++++++++++++++-- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index aac6cd386c..559883f224 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -86,20 +86,29 @@ def get_batch_naming_series(): class Batch(Document): def autoname(self): """Generate random ID for batch if not specified""" - if not self.batch_id: - create_new_batch, batch_number_series = frappe.db.get_value( - "Item", self.item, ["create_new_batch", "batch_number_series"] - ) - if create_new_batch: - if batch_number_series: - self.batch_id = make_autoname(batch_number_series, doc=self) - elif batch_uses_naming_series(): - self.batch_id = self.get_name_from_naming_series() - else: - self.batch_id = get_name_from_hash() + if self.batch_id: + self.name = self.batch_id + return + + create_new_batch, batch_number_series = frappe.db.get_value( + "Item", self.item, ["create_new_batch", "batch_number_series"] + ) + + if not create_new_batch: + frappe.throw(_("Batch ID is mandatory"), frappe.MandatoryError) + + while not self.batch_id: + if batch_number_series: + self.batch_id = make_autoname(batch_number_series, doc=self) + elif batch_uses_naming_series(): + self.batch_id = self.get_name_from_naming_series() else: - frappe.throw(_("Batch ID is mandatory"), frappe.MandatoryError) + self.batch_id = get_name_from_hash() + + # User might have manually created a batch with next number + if frappe.db.exists("Batch", self.batch_id): + self.batch_id = None self.name = self.batch_id diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index c76da626b5..3e470d4ce4 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -11,6 +11,8 @@ from frappe.utils.data import add_to_date, getdate from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.stock.doctype.batch.batch import UnableToSelectBatchError, get_batch_no, get_batch_qty +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_stock_reconciliation, @@ -27,7 +29,7 @@ class TestBatch(FrappeTestCase): ) @classmethod - def make_batch_item(cls, item_name): + def make_batch_item(cls, item_name=None): from erpnext.stock.doctype.item.test_item import make_item if not frappe.db.exists(item_name): @@ -245,7 +247,7 @@ class TestBatch(FrappeTestCase): if not use_naming_series: frappe.set_value("Stock Settings", "Stock Settings", "use_naming_series", 0) - def make_new_batch(self, item_name, batch_id=None, do_not_insert=0): + def make_new_batch(self, item_name=None, batch_id=None, do_not_insert=0): batch = frappe.new_doc("Batch") item = self.make_batch_item(item_name) batch.item = item.name @@ -407,6 +409,26 @@ class TestBatch(FrappeTestCase): self.assertEqual(getdate(batch.expiry_date), getdate(expiry_date)) + def test_autocreation_of_batches(self): + """ + Test if auto created Serial No excludes existing serial numbers + """ + item_code = make_item( + properties={ + "has_batch_no": 1, + "batch_number_series": "BATCHEXISTING.###", + "create_new_batch": 1, + } + ).name + + manually_created_batch = self.make_new_batch(item_code, batch_id="BATCHEXISTING001").name + + pr_1 = make_purchase_receipt(item_code=item_code, qty=1, batch_no=manually_created_batch) + pr_2 = make_purchase_receipt(item_code=item_code, qty=1) + + self.assertNotEqual(pr_1.items[0].batch_no, pr_2.items[0].batch_no) + self.assertEqual("BATCHEXISTING002", pr_2.items[0].batch_no) + def create_batch(item_code, rate, create_item_price_for_batch): pi = make_purchase_invoice( From 935e5b1dcd16acefde31a6f8187d2a633fe667aa Mon Sep 17 00:00:00 2001 From: maharshivpatel <39730881+maharshivpatel@users.noreply.github.com> Date: Fri, 27 May 2022 11:48:55 +0530 Subject: [PATCH 081/192] fix(india): duplicate qrcode and hide button (#31100) --- erpnext/regional/india/e_invoice/einvoice.js | 47 +++++++++++++------- erpnext/regional/india/e_invoice/utils.py | 32 ++++++++++--- 2 files changed, 57 insertions(+), 22 deletions(-) diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index 4748b265dc..ef24ce791c 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -11,7 +11,7 @@ erpnext.setup_einvoice_actions = (doctype) => { if (!invoice_eligible) return; - const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc; + const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, qrcode_image, __unsaved } = frm.doc; const add_custom_button = (label, action) => { if (!frm.custom_buttons[label]) { @@ -175,27 +175,44 @@ erpnext.setup_einvoice_actions = (doctype) => { } if (irn && !irn_cancelled) { - const action = () => { - const dialog = frappe.msgprint({ - title: __("Generate QRCode"), - message: __("Generate and attach QR Code using IRN?"), - primary_action: { - action: function() { - frappe.call({ - method: 'erpnext.regional.india.e_invoice.utils.generate_qrcode', - args: { doctype, docname: name }, - freeze: true, - callback: () => frm.reload_doc() || dialog.hide(), - error: () => dialog.hide() - }); + let is_qrcode_attached = false; + if (qrcode_image && frm.attachments) { + let attachments = frm.attachments.get_attachments(); + if (attachments.length != 0) { + for (let i = 0; i < attachments.length; i++) { + if (attachments[i].file_url == qrcode_image) { + is_qrcode_attached = true; + break; } - }, + } + } + } + if (!is_qrcode_attached) { + const action = () => { + if (frm.doc.__unsaved) { + frappe.throw(__('Please save the document to generate QRCode.')); + } + const dialog = frappe.msgprint({ + title: __("Generate QRCode"), + message: __("Generate and attach QR Code using IRN?"), + primary_action: { + action: function() { + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.generate_qrcode', + args: { doctype, docname: name }, + freeze: true, + callback: () => frm.reload_doc() || dialog.hide(), + error: () => dialog.hide() + }); + } + }, primary_action_label: __('Yes') }); dialog.show(); }; add_custom_button(__("Generate QRCode"), action); } + } } }); }; diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index bcb3e4fb85..e5a1a59e42 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -1010,13 +1010,32 @@ class GSPConnector: return failed def fetch_and_attach_qrcode_from_irn(self): - qrcode = self.get_qrcode_from_irn(self.invoice.irn) - if qrcode: - qrcode_file = self.create_qr_code_file(qrcode) - frappe.db.set_value("Sales Invoice", self.invoice.name, "qrcode_image", qrcode_file.file_url) - frappe.msgprint(_("QR Code attached to the invoice"), alert=True) + is_qrcode_file_attached = self.invoice.qrcode_image and frappe.db.exists( + "File", + { + "attached_to_doctype": "Sales Invoice", + "attached_to_name": self.invoice.name, + "file_url": self.invoice.qrcode_image, + "attached_to_field": "qrcode_image", + }, + ) + if not is_qrcode_file_attached: + if self.invoice.signed_qr_code: + self.attach_qrcode_image() + frappe.db.set_value( + "Sales Invoice", self.invoice.name, "qrcode_image", self.invoice.qrcode_image + ) + frappe.msgprint(_("QR Code attached to the invoice."), alert=True) + else: + qrcode = self.get_qrcode_from_irn(self.invoice.irn) + if qrcode: + qrcode_file = self.create_qr_code_file(qrcode) + frappe.db.set_value("Sales Invoice", self.invoice.name, "qrcode_image", qrcode_file.file_url) + frappe.msgprint(_("QR Code attached to the invoice."), alert=True) + else: + frappe.msgprint(_("QR Code not found for the IRN"), alert=True) else: - frappe.msgprint(_("QR Code not found for the IRN"), alert=True) + frappe.msgprint(_("QR Code is already Attached"), indicator="green", alert=True) def get_qrcode_from_irn(self, irn): import requests @@ -1281,7 +1300,6 @@ class GSPConnector: def attach_qrcode_image(self): qrcode = self.invoice.signed_qr_code - qr_image = io.BytesIO() url = qrcreate(qrcode, error="L") url.png(qr_image, scale=2, quiet_zone=1) From 2a10f09d8dc221b8e8c7c519bf3f56405024afff Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 27 May 2022 12:12:34 +0530 Subject: [PATCH 082/192] fix: Exchange rate reste to 1 on making mapped doc --- erpnext/public/js/controllers/transaction.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 05a401bdee..d11205a1ad 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -944,7 +944,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } else { // company currency and doc currency is same // this will prevent unnecessary conversion rate triggers - this.frm.set_value("conversion_rate", 1.0); + if(this.frm.doc.currency === this.get_company_currency()) { + this.frm.set_value("conversion_rate", 1.0); + } else { + this.conversion_rate(); + } } } From 4b04694c2c7b0ad9b1b59b34f0b3d5eb8e063625 Mon Sep 17 00:00:00 2001 From: HarryPaulo Date: Fri, 27 May 2022 03:46:07 -0300 Subject: [PATCH 083/192] fix(pos): freeze screen while processing pos invoices (#30850) --- .../pos_closing_entry/pos_closing_entry.js | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js index 572410fc66..98f3420d87 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js @@ -102,7 +102,9 @@ frappe.ui.form.on('POS Closing Entry', { }); }, - before_save: function(frm) { + before_save: async function(frm) { + frappe.dom.freeze(__('Processing Sales! Please Wait...')); + frm.set_value("grand_total", 0); frm.set_value("net_total", 0); frm.set_value("total_quantity", 0); @@ -112,17 +114,23 @@ frappe.ui.form.on('POS Closing Entry', { row.expected_amount = row.opening_amount; } - for (let row of frm.doc.pos_transactions) { - frappe.db.get_doc("POS Invoice", row.pos_invoice).then(doc => { - frm.doc.grand_total += flt(doc.grand_total); - frm.doc.net_total += flt(doc.net_total); - frm.doc.total_quantity += flt(doc.total_qty); - refresh_payments(doc, frm); - refresh_taxes(doc, frm); - refresh_fields(frm); - set_html_data(frm); - }); + const pos_inv_promises = frm.doc.pos_transactions.map( + row => frappe.db.get_doc("POS Invoice", row.pos_invoice) + ); + + const pos_invoices = await Promise.all(pos_inv_promises); + + for (let doc of pos_invoices) { + frm.doc.grand_total += flt(doc.grand_total); + frm.doc.net_total += flt(doc.net_total); + frm.doc.total_quantity += flt(doc.total_qty); + refresh_payments(doc, frm); + refresh_taxes(doc, frm); + refresh_fields(frm); + set_html_data(frm); } + + frappe.dom.unfreeze(); } }); From 385e22a06725f73a45f60aeb88620bade89ba528 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 27 May 2022 12:57:07 +0530 Subject: [PATCH 084/192] fix: Gratuity status not updated on salary slip submission --- erpnext/payroll/doctype/salary_slip/salary_slip.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 6a7f72b013..f4f84155af 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -116,10 +116,10 @@ class SalarySlip(TransactionBase): self.update_payment_status_for_gratuity() def update_payment_status_for_gratuity(self): - add_salary = frappe.db.get_all( + additional_salary = frappe.db.get_all( "Additional Salary", filters={ - "payroll_date": ("BETWEEN", [self.start_date, self.end_date]), + "payroll_date": ("between", [self.start_date, self.end_date]), "employee": self.employee, "ref_doctype": "Gratuity", "docstatus": 1, @@ -128,10 +128,10 @@ class SalarySlip(TransactionBase): limit=1, ) - if len(add_salary): + if additional_salary: status = "Paid" if self.docstatus == 1 else "Unpaid" - if add_salary[0].name in [data.additional_salary for data in self.earnings]: - frappe.db.set_value("Gratuity", add_salary.ref_docname, "status", status) + if additional_salary[0].name in [entry.additional_salary for entry in self.earnings]: + frappe.db.set_value("Gratuity", additional_salary[0].ref_docname, "status", status) def on_cancel(self): self.set_status() From b81d7519c1c5c0d24e42a26c9175ac6e14511133 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 27 May 2022 12:58:10 +0530 Subject: [PATCH 085/192] test: Gratuity status for payment via salary slip --- .../payroll/doctype/gratuity/test_gratuity.py | 23 ++++++++++++++++--- .../doctype/salary_slip/test_salary_slip.py | 10 +++++--- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py index aa03d80d63..5955758a30 100644 --- a/erpnext/payroll/doctype/gratuity/test_gratuity.py +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -4,7 +4,8 @@ import unittest import frappe -from frappe.utils import add_days, flt, get_datetime, getdate +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days, add_months, flt, get_datetime, get_first_day, getdate from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.expense_claim.test_expense_claim import get_payable_account @@ -14,14 +15,16 @@ from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( make_earning_salary_component, make_employee_salary_slip, ) +from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule test_dependencies = ["Salary Component", "Salary Slip", "Account"] -class TestGratuity(unittest.TestCase): +class TestGratuity(FrappeTestCase): def setUp(self): frappe.db.delete("Gratuity") + frappe.db.delete("Salary Slip") frappe.db.delete("Additional Salary", {"ref_doctype": "Gratuity"}) make_earning_salary_component( @@ -76,6 +79,14 @@ class TestGratuity(unittest.TestCase): # additional salary creation (Pay via salary slip) self.assertTrue(frappe.db.exists("Additional Salary", {"ref_docname": gratuity.name})) + salary_slip = make_salary_slip("Test Gratuity", employee=employee) + salary_slip.posting_date = getdate() + salary_slip.insert() + salary_slip.submit() + + gratuity.reload() + self.assertEqual(gratuity.status, "Paid") + def test_check_gratuity_amount_based_on_all_previous_slabs(self): employee, sal_slip = create_employee_and_get_last_salary_slip() rule = get_gratuity_rule("Rule Under Limited Contract (UAE)") @@ -209,7 +220,13 @@ def create_employee_and_get_last_salary_slip(): frappe.db.set_value("Employee", employee, "relieving_date", getdate()) frappe.db.set_value("Employee", employee, "date_of_joining", add_days(getdate(), -(6 * 365))) if not frappe.db.exists("Salary Slip", {"employee": employee}): - salary_slip = make_employee_salary_slip("test_employee@salary.com", "Monthly") + posting_date = get_first_day(add_months(getdate(), -1)) + salary_slip = make_employee_salary_slip( + "test_employee@salary.com", "Monthly", "Test Gratuity", posting_date=posting_date + ) + salary_slip.start_date = posting_date + salary_slip.end_date = None + salary_slip.save() salary_slip.submit() salary_slip = salary_slip.name else: diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 1bc3741922..60ba2d9a07 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -997,7 +997,7 @@ class TestSalarySlip(unittest.TestCase): return [no_of_days_in_month[1], no_of_holidays_in_month] -def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): +def make_employee_salary_slip(user, payroll_frequency, salary_structure=None, posting_date=None): from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure if not salary_structure: @@ -1008,7 +1008,11 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): ) salary_structure_doc = make_salary_structure( - salary_structure, payroll_frequency, employee=employee.name, company=employee.company + salary_structure, + payroll_frequency, + employee=employee.name, + company=employee.company, + from_date=posting_date, ) salary_slip_name = frappe.db.get_value( "Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})} @@ -1018,7 +1022,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): salary_slip = make_salary_slip(salary_structure_doc.name, employee=employee.name) salary_slip.employee_name = employee.employee_name salary_slip.payroll_frequency = payroll_frequency - salary_slip.posting_date = nowdate() + salary_slip.posting_date = posting_date or nowdate() salary_slip.insert() else: salary_slip = frappe.get_doc("Salary Slip", salary_slip_name) From 6c66bbbbfeb7cec913684ae7d276d2266932e0f0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 27 May 2022 13:39:25 +0530 Subject: [PATCH 086/192] refactor: clean-up gratuity tests --- .../payroll/doctype/gratuity/test_gratuity.py | 132 ++++++++---------- 1 file changed, 59 insertions(+), 73 deletions(-) diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py index 5955758a30..cbc64f1c8d 100644 --- a/erpnext/payroll/doctype/gratuity/test_gratuity.py +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -5,10 +5,11 @@ import unittest import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import add_days, add_months, flt, get_datetime, get_first_day, getdate +from frappe.utils import add_days, add_months, floor, flt, get_datetime, get_first_day, getdate from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.expense_claim.test_expense_claim import get_payable_account +from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list from erpnext.payroll.doctype.gratuity.gratuity import get_last_salary_slip from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( make_deduction_salary_component, @@ -32,32 +33,38 @@ class TestGratuity(FrappeTestCase): ) make_deduction_salary_component(setup=True, test_tax=True, company_list=["_Test Company"]) + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") def test_get_last_salary_slip_should_return_none_for_new_employee(self): new_employee = make_employee("new_employee@salary.com", company="_Test Company") salary_slip = get_last_salary_slip(new_employee) - assert salary_slip is None + self.assertIsNone(salary_slip) - def test_check_gratuity_amount_based_on_current_slab_and_additional_salary_creation(self): - employee, sal_slip = create_employee_and_get_last_salary_slip() + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") + def test_gratuity_based_on_current_slab_via_additional_salary(self): + """ + Range | Fraction + 5-0 | 1 + """ + doj = add_days(getdate(), -(6 * 365)) + relieving_date = getdate() + + employee = make_employee( + "test_employee_gratuity@salary.com", + company="_Test Company", + date_of_joining=doj, + relieving_date=relieving_date, + ) + sal_slip = create_salary_slip("test_employee_gratuity@salary.com") rule = get_gratuity_rule("Rule Under Unlimited Contract on termination (UAE)") gratuity = create_gratuity(pay_via_salary_slip=1, employee=employee, rule=rule.name) # work experience calculation - date_of_joining, relieving_date = frappe.db.get_value( - "Employee", employee, ["date_of_joining", "relieving_date"] - ) - employee_total_workings_days = ( - get_datetime(relieving_date) - get_datetime(date_of_joining) - ).days + employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(doj)).days + experience = floor(employee_total_workings_days / rule.total_working_days_per_year) + self.assertEqual(gratuity.current_work_experience, experience) - experience = employee_total_workings_days / rule.total_working_days_per_year - gratuity.reload() - from math import floor - - self.assertEqual(floor(experience), gratuity.current_work_experience) - - # amount Calculation + # amount calculation component_amount = frappe.get_all( "Salary Detail", filters={ @@ -67,18 +74,15 @@ class TestGratuity(FrappeTestCase): "salary_component": "Basic Salary", }, fields=["amount"], + limit=1, ) - - """ 5 - 0 fraction is 1 """ - gratuity_amount = component_amount[0].amount * experience - gratuity.reload() - self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2)) # additional salary creation (Pay via salary slip) self.assertTrue(frappe.db.exists("Additional Salary", {"ref_docname": gratuity.name})) + # gratuity should be marked "Paid" on the next salary slip submission salary_slip = make_salary_slip("Test Gratuity", employee=employee) salary_slip.posting_date = getdate() salary_slip.insert() @@ -87,8 +91,27 @@ class TestGratuity(FrappeTestCase): gratuity.reload() self.assertEqual(gratuity.status, "Paid") - def test_check_gratuity_amount_based_on_all_previous_slabs(self): - employee, sal_slip = create_employee_and_get_last_salary_slip() + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") + def test_gratuity_based_on_all_previous_slabs_via_payment_entry(self): + """ + Range | Fraction + 0-1 | 0 + 1-5 | 0.7 + 5-0 | 1 + """ + from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry + + doj = add_days(getdate(), -(6 * 365)) + relieving_date = getdate() + + employee = make_employee( + "test_employee_gratuity@salary.com", + company="_Test Company", + date_of_joining=doj, + relieving_date=relieving_date, + ) + + sal_slip = create_salary_slip("test_employee_gratuity@salary.com") rule = get_gratuity_rule("Rule Under Limited Contract (UAE)") set_mode_of_payment_account() @@ -97,22 +120,11 @@ class TestGratuity(FrappeTestCase): ) # work experience calculation - date_of_joining, relieving_date = frappe.db.get_value( - "Employee", employee, ["date_of_joining", "relieving_date"] - ) - employee_total_workings_days = ( - get_datetime(relieving_date) - get_datetime(date_of_joining) - ).days + employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(doj)).days + experience = floor(employee_total_workings_days / rule.total_working_days_per_year) + self.assertEqual(gratuity.current_work_experience, experience) - experience = employee_total_workings_days / rule.total_working_days_per_year - - gratuity.reload() - - from math import floor - - self.assertEqual(floor(experience), gratuity.current_work_experience) - - # amount Calculation + # amount calculation component_amount = frappe.get_all( "Salary Detail", filters={ @@ -122,35 +134,22 @@ class TestGratuity(FrappeTestCase): "salary_component": "Basic Salary", }, fields=["amount"], + limit=1, ) - """ range | Fraction - 0-1 | 0 - 1-5 | 0.7 - 5-0 | 1 - """ - gratuity_amount = ((0 * 1) + (4 * 0.7) + (1 * 1)) * component_amount[0].amount - gratuity.reload() - self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2)) self.assertEqual(gratuity.status, "Unpaid") - from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry + pe = get_payment_entry("Gratuity", gratuity.name) + pe.reference_no = "123467" + pe.reference_date = getdate() + pe.submit() - pay_entry = get_payment_entry("Gratuity", gratuity.name) - pay_entry.reference_no = "123467" - pay_entry.reference_date = getdate() - pay_entry.save() - pay_entry.submit() gratuity.reload() - self.assertEqual(gratuity.status, "Paid") self.assertEqual(flt(gratuity.paid_amount, 2), flt(gratuity.amount, 2)) - def tearDown(self): - frappe.db.rollback() - def get_gratuity_rule(name): rule = frappe.db.exists("Gratuity Rule", name) @@ -160,7 +159,6 @@ def get_gratuity_rule(name): rule.applicable_earnings_component = [] rule.append("applicable_earnings_component", {"salary_component": "Basic Salary"}) rule.save() - rule.reload() return rule @@ -215,29 +213,17 @@ def create_account(): ).insert(ignore_permissions=True) -def create_employee_and_get_last_salary_slip(): - employee = make_employee("test_employee@salary.com", company="_Test Company") - frappe.db.set_value("Employee", employee, "relieving_date", getdate()) - frappe.db.set_value("Employee", employee, "date_of_joining", add_days(getdate(), -(6 * 365))) +def create_salary_slip(employee): if not frappe.db.exists("Salary Slip", {"employee": employee}): posting_date = get_first_day(add_months(getdate(), -1)) salary_slip = make_employee_salary_slip( - "test_employee@salary.com", "Monthly", "Test Gratuity", posting_date=posting_date + employee, "Monthly", "Test Gratuity", posting_date=posting_date ) salary_slip.start_date = posting_date salary_slip.end_date = None - salary_slip.save() salary_slip.submit() salary_slip = salary_slip.name else: salary_slip = get_last_salary_slip(employee) - if not frappe.db.get_value("Employee", "test_employee@salary.com", "holiday_list"): - from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list - - make_holiday_list() - frappe.db.set_value( - "Company", "_Test Company", "default_holiday_list", "Salary Slip Test Holiday List" - ) - - return employee, salary_slip + return salary_slip From 79b0aede00ac83c0b99015e5d3fd2ac8a62b1c23 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 27 May 2022 13:57:09 +0530 Subject: [PATCH 087/192] fix: add list view settings for Gratuity --- erpnext/payroll/doctype/gratuity/gratuity.json | 7 +++---- erpnext/payroll/doctype/gratuity/gratuity_list.js | 12 ++++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 erpnext/payroll/doctype/gratuity/gratuity_list.js diff --git a/erpnext/payroll/doctype/gratuity/gratuity.json b/erpnext/payroll/doctype/gratuity/gratuity.json index 1fd1cecaaa..c540baf7e6 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.json +++ b/erpnext/payroll/doctype/gratuity/gratuity.json @@ -76,9 +76,8 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Status", - "options": "Draft\nUnpaid\nPaid", - "read_only": 1, - "reqd": 1 + "options": "Draft\nUnpaid\nPaid\nSubmitted\nCancelled", + "read_only": 1 }, { "depends_on": "eval: !doc.pay_via_salary_slip", @@ -194,7 +193,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-02-02 14:00:45.536152", + "modified": "2022-05-27 13:56:14.349183", "modified_by": "Administrator", "module": "Payroll", "name": "Gratuity", diff --git a/erpnext/payroll/doctype/gratuity/gratuity_list.js b/erpnext/payroll/doctype/gratuity/gratuity_list.js new file mode 100644 index 0000000000..20e3d5b4e5 --- /dev/null +++ b/erpnext/payroll/doctype/gratuity/gratuity_list.js @@ -0,0 +1,12 @@ +frappe.listview_settings["Gratuity"] = { + get_indicator: function(doc) { + let status_color = { + "Draft": "red", + "Submitted": "blue", + "Cancelled": "red", + "Paid": "green", + "Unpaid": "orange", + }; + return [__(doc.status), status_color[doc.status], "status,=,"+doc.status]; + } +}; \ No newline at end of file From eabd8290d40db9745a046c422ed17e024a685ae2 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 25 May 2022 15:32:42 +0530 Subject: [PATCH 088/192] feat: Only update exploded items rate and amount - Generate RM-Rate map from Items table (will include subassembly items with rate) - Function to reset exploded item rate from above map - `db_update` exploded item rate only if rate is changed - Via Update Cost, only update exploded items rate, do not regenerate table again - Exploded Items are regenerated on Save and Replace BOM job - `calculate_exploded_cost` is run only via non doc events (Update Cost button, Update BOMs Cost Job) --- erpnext/manufacturing/doctype/bom/bom.py | 39 +++++++++++++++++-- .../bom_explosion_item.json | 4 +- .../bom_update_log/bom_updation_utils.py | 1 - 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 560019a86d..6d53cfe707 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -5,7 +5,7 @@ import functools import re from collections import deque from operator import itemgetter -from typing import List +from typing import Dict, List import frappe from frappe import _ @@ -185,6 +185,7 @@ class BOM(WebsiteGenerator): self.validate_transfer_against() self.set_routing_operations() self.validate_operations() + self.update_exploded_items(save=False) self.calculate_cost() self.update_stock_qty() self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False) @@ -391,8 +392,6 @@ class BOM(WebsiteGenerator): if save: self.db_update() - self.update_exploded_items(save=save) - # update parent BOMs if self.total_cost != existing_bom_cost and update_parent: parent_boms = frappe.db.sql_list( @@ -594,6 +593,10 @@ class BOM(WebsiteGenerator): self.calculate_op_cost(update_hour_rate) self.calculate_rm_cost(save=save_updates) self.calculate_sm_cost(save=save_updates) + if save_updates: + # not via doc event, table is not regenerated and needs updation + self.calculate_exploded_cost() + self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost self.base_total_cost = ( self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost @@ -689,6 +692,36 @@ class BOM(WebsiteGenerator): self.scrap_material_cost = total_sm_cost self.base_scrap_material_cost = base_total_sm_cost + def calculate_exploded_cost(self): + "Set exploded row cost from it's parent BOM." + rm_rate_map = self.get_rm_rate_map() + + for row in self.get("exploded_items"): + old_rate = flt(row.rate) + row.rate = rm_rate_map.get(row.item_code) + row.amount = flt(row.stock_qty) * row.rate + + if old_rate != row.rate: + # Only db_update if unchanged + row.db_update() + + def get_rm_rate_map(self) -> Dict[str, float]: + "Create Raw Material-Rate map for Exploded Items. Fetch rate from Items table or Subassembly BOM." + rm_rate_map = {} + + for item in self.get("items"): + if item.bom_no: + # Get Item-Rate from Subassembly BOM + explosion_items = frappe.db.get_all( + "BOM Explosion Item", filters={"parent": item.bom_no}, fields=["item_code", "rate"] + ) + explosion_item_rate = {item.item_code: flt(item.rate) for item in explosion_items} + rm_rate_map.update(explosion_item_rate) + else: + rm_rate_map[item.item_code] = flt(item.base_rate) / flt(item.conversion_factor or 1.0) + + return rm_rate_map + def update_exploded_items(self, save=True): """Update Flat BOM, following will be correct data""" self.get_exploded_items() diff --git a/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json b/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json index f01d856e72..9b1db63494 100644 --- a/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json +++ b/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json @@ -169,13 +169,15 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-10-08 16:21:29.386212", + "modified": "2022-05-27 13:42:23.305455", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Explosion Item", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py index d246d3064f..1ec15f0d3a 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py @@ -120,7 +120,6 @@ def update_cost_in_boms(bom_list: List[str], docname: str) -> Dict[str, Dict]: for bom in bom_list: bom_doc = frappe.get_cached_doc("BOM", bom) bom_doc.calculate_cost(save_updates=True, update_hour_rate=True) - # bom_doc.update_exploded_items(save=True) #TODO: edit exploded items rate bom_doc.db_update() updated_boms[bom] = True From c9e070393d85f07e676c39bb913ac72054f6ff04 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 27 May 2022 14:42:11 +0530 Subject: [PATCH 089/192] test: make holiday list before running gratuity tests --- erpnext/payroll/doctype/gratuity/test_gratuity.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py index cbc64f1c8d..1155a06edd 100644 --- a/erpnext/payroll/doctype/gratuity/test_gratuity.py +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -15,6 +15,7 @@ from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( make_deduction_salary_component, make_earning_salary_component, make_employee_salary_slip, + make_holiday_list, ) from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule @@ -32,6 +33,7 @@ class TestGratuity(FrappeTestCase): setup=True, test_tax=True, company_list=["_Test Company"], include_flexi_benefits=True ) make_deduction_salary_component(setup=True, test_tax=True, company_list=["_Test Company"]) + make_holiday_list() @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") def test_get_last_salary_slip_should_return_none_for_new_employee(self): From 59499462650965b483a2b0f9f377d59b98f756e6 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 27 May 2022 17:04:21 +0530 Subject: [PATCH 090/192] chore: Change BOM Progress field types to Long Text --- erpnext/manufacturing/doctype/bom/bom.py | 2 +- .../doctype/bom_update_log/bom_update_log.json | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 6d53cfe707..15048ec990 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -702,7 +702,7 @@ class BOM(WebsiteGenerator): row.amount = flt(row.stock_qty) * row.rate if old_rate != row.rate: - # Only db_update if unchanged + # Only db_update if changed row.db_update() def get_rm_rate_map(self) -> Dict[str, float]: diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json index db5f58d04f..bea3cf0373 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json @@ -76,18 +76,18 @@ }, { "fieldname": "current_boms", - "fieldtype": "Text", + "fieldtype": "Long Text", "label": "Current BOMs" }, { "description": "Immediate parent BOMs", "fieldname": "parent_boms", - "fieldtype": "Text", + "fieldtype": "Long Text", "label": "Parent BOMs" }, { "fieldname": "processed_boms", - "fieldtype": "Text", + "fieldtype": "Long Text", "label": "Processed BOMs" } ], @@ -95,7 +95,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-05-24 17:52:21.824710", + "modified": "2022-05-27 17:03:34.712010", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Update Log", From 7ff8acac517bf11a0c17bb399d1de11a8df30976 Mon Sep 17 00:00:00 2001 From: MOHAMMED NIYAS <76736615+niyazrazak@users.noreply.github.com> Date: Fri, 27 May 2022 17:13:14 +0530 Subject: [PATCH 091/192] fix: date filter on quality inspection report (#31148) * fix: date filter fix from date to to date filter btw those days * fix: remove unnecessary conditions Co-authored-by: Ankush Menat --- .../quality_inspection_summary/quality_inspection_summary.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py index 0a79130f1b..c324172372 100644 --- a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py +++ b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py @@ -34,8 +34,8 @@ def get_data(filters): if filters.get(field): query_filters[field] = ("in", filters.get(field)) - query_filters["report_date"] = (">=", filters.get("from_date")) - query_filters["report_date"] = ("<=", filters.get("to_date")) + + query_filters["report_date"] = ["between", [filters.get("from_date"), filters.get("to_date")]] return frappe.get_all( "Quality Inspection", fields=fields, filters=query_filters, order_by="report_date asc" From ce8e05146eabd067ff6b9a238fc3b4c7be245bd2 Mon Sep 17 00:00:00 2001 From: HENRY Florian Date: Fri, 27 May 2022 13:57:43 +0200 Subject: [PATCH 092/192] chore: update translation fr for BOM (#31126) * fix: update translation * fix: fr translation for BOM --- erpnext/translations/fr.csv | 151 ++++++++++++++++++------------------ 1 file changed, 77 insertions(+), 74 deletions(-) diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv index 8518156eb2..22e3c356d6 100644 --- a/erpnext/translations/fr.csv +++ b/erpnext/translations/fr.csv @@ -175,7 +175,7 @@ Airline,Compagnie aérienne, All Accounts,Tous les comptes, All Addresses.,Toutes les adresses., All Assessment Groups,Tous les Groupes d'Évaluation, -All BOMs,Toutes les LDM, +All BOMs,Toutes les nomenclatures, All Contacts.,Tous les contacts., All Customer Groups,Tous les Groupes Client, All Day,Toute la Journée, @@ -330,16 +330,16 @@ Avg Daily Outgoing,Moy Quotidienne Sortante, Avg. Buying Price List Rate,Moyenne de la liste de prix d'achat, Avg. Selling Price List Rate,Prix moyen de la liste de prix de vente, Avg. Selling Rate,Moy. Taux de vente, -BOM,LDM (Liste de Matériaux), -BOM Browser,Explorateur LDM, -BOM No,N° LDM, -BOM Rate,Taux LDM, -BOM Stock Report,Rapport de Stock de LDM, -BOM and Manufacturing Quantity are required,LDM et quantité de production sont nécessaires, -BOM does not contain any stock item,LDM ne contient aucun article en stock, -BOM {0} does not belong to Item {1},LDM {0} n’appartient pas à l'article {1}, -BOM {0} must be active,LDM {0} doit être active, -BOM {0} must be submitted,LDM {0} doit être soumise, +BOM,Nomenclature, +BOM Browser,Explorateur Nomenclature, +BOM No,N° Nomenclature, +BOM Rate,Valeur nomenclature, +BOM Stock Report,Rapport de Stock des nomenclatures, +BOM and Manufacturing Quantity are required,Nomenclature et quantité de production sont nécessaires, +BOM does not contain any stock item,Nomenclature ne contient aucun article en stock, +BOM {0} does not belong to Item {1},Nomenclature {0} n’appartient pas à l'article {1}, +BOM {0} must be active,Nomenclature {0} doit être active, +BOM {0} must be submitted,Nomenclature {0} doit être soumise, Balance,Solde, Balance (Dr - Cr),Balance (Dr - Cr), Balance ({0}),Solde ({0}), @@ -386,8 +386,8 @@ Beginner,Débutant, Bill,Facture, Bill Date,Date de la Facture, Bill No,Numéro de facture, -Bill of Materials,Liste de Matériaux, -Bill of Materials (BOM),Liste de Matériaux (LDM), +Bill of Materials,Nomenclatures, +Bill of Materials (BOM),Nomenclature, Billable Hours,Heures facturables, Billed,Facturé, Billed Amount,Montant facturé, @@ -404,14 +404,14 @@ Birthday Reminder,Rappel d'anniversaire, Black,Noir, Blanket Orders from Costumers.,Commandes provisoires de clients., Block Invoice,Bloquer la facture, -Boms,Listes de Matériaux, +Boms,Nomenclatures, Bonus Payment Date cannot be a past date,La date de paiement du bonus ne peut pas être une date passée, Both Trial Period Start Date and Trial Period End Date must be set,La date de début de la période d'essai et la date de fin de la période d'essai doivent être définies, Both Warehouse must belong to same Company,Les deux Entrepôt doivent appartenir à la même Société, Branch,Branche, Broadcasting,Radio/Télévision, Brokerage,Courtage, -Browse BOM,Parcourir la LDM, +Browse BOM,Parcourir la nomenclature, Budget Against,Budget Pour, Budget List,Liste budgétaire, Budget Variance Report,Rapport d’Écarts de Budget, @@ -467,7 +467,7 @@ Cannot convert Cost Center to ledger as it has child nodes,Conversion impossible Cannot covert to Group because Account Type is selected.,Conversion impossible en Groupe car le Type de Compte est sélectionné., Cannot create Retention Bonus for left Employees,Impossible de créer une prime de fidélisation pour les employés ayant quitté l'entreprise, Cannot create a Delivery Trip from Draft documents.,Impossible de créer un voyage de livraison à partir de documents brouillons., -Cannot deactivate or cancel BOM as it is linked with other BOMs,Désactivation ou annulation de la LDM impossible car elle est liée avec d'autres LDMs, +Cannot deactivate or cancel BOM as it is linked with other BOMs,Désactivation ou annulation de la nomenclature impossible car elle est liée avec d'autres nomenclatures, "Cannot declare as lost, because Quotation has been made.","Impossible de déclarer comme perdu, parce que le Devis a été fait.", Cannot deduct when category is for 'Valuation' or 'Valuation and Total',Déduction impossible lorsque la catégorie est pour 'Évaluation' ou 'Vaulation et Total', Cannot deduct when category is for 'Valuation' or 'Vaulation and Total',Vous ne pouvez pas déduire lorsqu'une catégorie est pour 'Évaluation' ou 'Évaluation et Total', @@ -722,7 +722,7 @@ Currency of the price list {0} must be {1} or {2},La devise de la liste de prix Currency should be same as Price List Currency: {0},La devise doit être la même que la devise de la liste de prix: {0}, Current,Actuel, Current Assets,Actifs Actuels, -Current BOM and New BOM can not be same,La LDM actuelle et la nouvelle LDM ne peuvent être pareilles, +Current BOM and New BOM can not be same,La nomenclature actuelle et la nouvelle nomenclature ne peuvent être pareilles, Current Job Openings,Offres d'Emploi Actuelles, Current Liabilities,Dettes Actuelles, Current Qty,Qté actuelle, @@ -780,9 +780,9 @@ Debtors ({0}),Débiteurs ({0}), Declare Lost,Déclarer perdu, Deduction,Déduction, Default Activity Cost exists for Activity Type - {0},Un Coût d’Activité par défault existe pour le Type d’Activité {0}, -Default BOM ({0}) must be active for this item or its template,LDM par défaut ({0}) doit être actif pour ce produit ou son modèle, -Default BOM for {0} not found,LDM par défaut {0} introuvable, -Default BOM not found for Item {0} and Project {1},La LDM par défaut n'a pas été trouvée pour l'Article {0} et le Projet {1}, +Default BOM ({0}) must be active for this item or its template,Nomenclature par défaut ({0}) doit être actif pour ce produit ou son modèle, +Default BOM for {0} not found,Nomenclature par défaut {0} introuvable, +Default BOM not found for Item {0} and Project {1},La nomenclature par défaut n'a pas été trouvée pour l'Article {0} et le Projet {1}, Default Letter Head,En-Tête de Courrier par Défaut, Default Tax Template,Modèle de Taxes par Défaut, Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM.,L’Unité de Mesure par Défaut pour l’Article {0} ne peut pas être modifiée directement parce que vous avez déjà fait une (des) transaction (s) avec une autre unité de mesure. Vous devez créer un nouvel article pour utiliser une UDM par défaut différente., @@ -1023,7 +1023,7 @@ Fees,Honoraires, Female,Féminin, Fetch Data,Récupérer des données, Fetch Subscription Updates,Vérifier les mises à jour des abonnements, -Fetch exploded BOM (including sub-assemblies),Récupérer la LDM éclatée (y compris les sous-ensembles), +Fetch exploded BOM (including sub-assemblies),Récupérer la nomenclature éclatée (y compris les sous-ensembles), Fetching records......,Récupération des enregistrements ......, Field Name,Nom du Champ, Fieldname,Nom du Champ, @@ -1135,7 +1135,7 @@ Get Employees,Obtenir des employés, Get Invocies,Obtenir des invocies, Get Invoices,Obtenir des factures, Get Invoices based on Filters,Obtenir les factures en fonction des filtres, -Get Items from BOM,Obtenir les Articles depuis LDM, +Get Items from BOM,Obtenir les Articles depuis nomenclature, Get Items from Healthcare Services,Obtenir des articles des services de santé, Get Items from Prescriptions,Obtenir des articles des prescriptions, Get Items from Product Bundle,Obtenir les Articles du Produit Groupé, @@ -1425,8 +1425,8 @@ Last Order Date,Date de la dernière commande, Last Purchase Price,Dernier prix d'achat, Last Purchase Rate,Dernier Prix d'Achat, Latest,Dernier, -Latest price updated in all BOMs,Prix les plus récents mis à jour dans toutes les LDMs, -Lead,Conduire, +Latest price updated in all BOMs,Prix les plus récents mis à jour dans toutes les nomenclatures, +Lead,Prospect, Lead Count,Nombre de Prospects, Lead Owner,Responsable du Prospect, Lead Owner cannot be same as the Lead,Le Responsable du Prospect ne peut pas être identique au Prospect, @@ -1655,7 +1655,7 @@ Net Total,Total net, Net pay cannot be negative,Salaire Net ne peut pas être négatif, New Account Name,Nouveau Nom de Compte, New Address,Nouvelle adresse, -New BOM,Nouvelle LDM, +New BOM,Nouvelle nomenclature, New Batch ID (Optional),Nouveau Numéro de Lot (Optionnel), New Batch Qty,Nouvelle Qté de Lot, New Company,Nouvelle Société, @@ -1689,7 +1689,7 @@ No Item with Serial No {0},Aucun Article avec le N° de Série {0}, No Items available for transfer,Aucun article disponible pour le transfert, No Items selected for transfer,Aucun article sélectionné pour le transfert, No Items to pack,Pas d’Articles à emballer, -No Items with Bill of Materials to Manufacture,Aucun Article avec une Liste de Matériel à Produire, +No Items with Bill of Materials to Manufacture,Aucun Article avec une nomenclature à Produire, No Items with Bill of Materials.,Aucun article avec nomenclature., No Permission,Aucune autorisation, No Remarks,Aucune Remarque, @@ -1777,7 +1777,7 @@ Online Auctions,Enchères en ligne, Only Leave Applications with status 'Approved' and 'Rejected' can be submitted,Seules les Demandes de Congés avec le statut 'Appouvée' ou 'Rejetée' peuvent être soumises, "Only the Student Applicant with the status ""Approved"" will be selected in the table below.",Seul les candidatures étudiantes avec le statut «Approuvé» seront sélectionnées dans le tableau ci-dessous., Only users with {0} role can register on Marketplace,Seuls les utilisateurs ayant le rôle {0} peuvent s'inscrire sur Marketplace, -Open BOM {0},Ouvrir LDM {0}, +Open BOM {0},Ouvrir nomenclature {0}, Open Item {0},Ouvrir l'Article {0}, Open Notifications,Notifications ouvertes, Open Orders,Commandes ouvertes, @@ -2015,9 +2015,9 @@ Please save the patient first,Veuillez d'abord enregistrer le patient, Please save the report again to rebuild or update,Veuillez enregistrer le rapport à nouveau pour reconstruire ou mettre à jour, "Please select Allocated Amount, Invoice Type and Invoice Number in atleast one row","Veuillez sélectionner le Montant Alloué, le Type de Facture et le Numéro de Facture dans au moins une ligne", Please select Apply Discount On,Veuillez sélectionnez Appliquer Remise Sur, -Please select BOM against item {0},Veuillez sélectionner la liste de matériaux (LDM) pour l'article {0}, -Please select BOM for Item in Row {0},Veuillez sélectionnez une LDM pour l’Article à la Ligne {0}, -Please select BOM in BOM field for Item {0},Veuillez sélectionner une LDM dans le champ LDM pour l’Article {0}, +Please select BOM against item {0},Veuillez sélectionner la nomenclature pour l'article {0}, +Please select BOM for Item in Row {0},Veuillez sélectionnez une nomenclature pour l’Article à la Ligne {0}, +Please select BOM in BOM field for Item {0},Veuillez sélectionner une nomenclature dans le champ nomenclature pour l’Article {0}, Please select Category first,Veuillez d’abord sélectionner une Catégorie, Please select Charge Type first,Veuillez d’abord sélectionner le Type de Facturation, Please select Company,Veuillez sélectionner une Société, @@ -2044,7 +2044,7 @@ Please select Qty against item {0},Veuillez sélectionner Qté par rapport à l' Please select Sample Retention Warehouse in Stock Settings first,Veuillez d'abord définir un entrepôt de stockage des échantillons dans les paramètres de stock, Please select Start Date and End Date for Item {0},Veuillez sélectionner la Date de Début et Date de Fin pour l'Article {0}, Please select Student Admission which is mandatory for the paid student applicant,Veuillez sélectionner obligatoirement une Admission d'Étudiant pour la candidature étudiante payée, -Please select a BOM,Veuillez sélectionner une LDM, +Please select a BOM,Veuillez sélectionner une nomenclature, Please select a Batch for Item {0}. Unable to find a single batch that fulfills this requirement,Veuillez sélectionner un Lot pour l'Article {0}. Impossible de trouver un seul lot satisfaisant à cette exigence, Please select a Company,Veuillez sélectionner une Société, Please select a batch,Veuillez sélectionner un lot, @@ -2273,8 +2273,8 @@ Quantity to Manufacture must be greater than 0.,La quantité à produire doit ê Quantity to Produce,Quantité à produire, Quantity to Produce can not be less than Zero,La quantité à produire ne peut être inférieure à zéro, Query Options,Options de Requête, -Queued for replacing the BOM. It may take a few minutes.,En file d'attente pour remplacer la LDM. Cela peut prendre quelques minutes., -Queued for updating latest price in all Bill of Materials. It may take a few minutes.,Mise à jour des prix les plus récents dans toutes les Listes de Matériaux en file d'attente. Cela peut prendre quelques minutes., +Queued for replacing the BOM. It may take a few minutes.,En file d'attente pour remplacer la nomenclature. Cela peut prendre quelques minutes., +Queued for updating latest price in all Bill of Materials. It may take a few minutes.,Mise à jour des prix les plus récents dans toutes les nomenclatures en file d'attente. Cela peut prendre quelques minutes., Quick Journal Entry,Écriture Rapide dans le Journal, Quot Count,Compte de Devis, Quot/Lead %,Devis / Prospects %, @@ -2354,7 +2354,7 @@ Reorder Level,Niveau de réapprovisionnement, Reorder Qty,Qté de Réapprovisionnement, Repeat Customer Revenue,Revenus de Clients Récurrents, Repeat Customers,Clients Récurrents, -Replace BOM and update latest price in all BOMs,Remplacer la LDM et actualiser les prix les plus récents dans toutes les LDMs, +Replace BOM and update latest price in all BOMs,Remplacer la nomenclature et actualiser les prix les plus récents dans toutes les nomenclatures, Replied,Répondu, Replies,réponses, Report,Rapport, @@ -2466,11 +2466,11 @@ Row {0}: Advance against Supplier must be debit,Ligne {0} : L’Avance du Fourni Row {0}: Allocated amount {1} must be less than or equals to Payment Entry amount {2},Ligne {0} : Le montant alloué {1} doit être inférieur ou égal au montant du Paiement {2}, Row {0}: Allocated amount {1} must be less than or equals to invoice outstanding amount {2},Ligne {0} : Le montant alloué {1} doit être inférieur ou égal au montant restant sur la Facture {2}, Row {0}: An Reorder entry already exists for this warehouse {1},Ligne {0} : Une écriture de Réapprovisionnement existe déjà pour cet entrepôt {1}, -Row {0}: Bill of Materials not found for the Item {1},Ligne {0} : Liste de Matériaux non trouvée pour l’Article {1}, +Row {0}: Bill of Materials not found for the Item {1},Ligne {0} : Nomenclature non trouvée pour l’Article {1}, Row {0}: Conversion Factor is mandatory,Ligne {0} : Le Facteur de Conversion est obligatoire, Row {0}: Cost center is required for an item {1},Ligne {0}: le Centre de Coûts est requis pour un article {1}, Row {0}: Credit entry can not be linked with a {1},Ligne {0} : L’Écriture de crédit ne peut pas être liée à un {1}, -Row {0}: Currency of the BOM #{1} should be equal to the selected currency {2},Ligne {0} : La devise de la LDM #{1} doit être égale à la devise sélectionnée {2}, +Row {0}: Currency of the BOM #{1} should be equal to the selected currency {2},Ligne {0} : La devise de la nomenclature #{1} doit être égale à la devise sélectionnée {2}, Row {0}: Debit entry can not be linked with a {1},Ligne {0} : L’Écriture de Débit ne peut pas être lié à un {1}, Row {0}: Depreciation Start Date is required,Ligne {0}: la date de début de l'amortissement est obligatoire, Row {0}: Enter location for the asset item {1},Ligne {0}: entrez la localisation de l'actif {1}, @@ -2490,7 +2490,7 @@ Row {0}: Please set the Mode of Payment in Payment Schedule,Ligne {0}: Veuillez Row {0}: Please set the correct code on Mode of Payment {1},Ligne {0}: définissez le code correct sur le mode de paiement {1}., Row {0}: Qty is mandatory,Ligne {0} : Qté obligatoire, Row {0}: Quality Inspection rejected for item {1},Ligne {0}: le contrôle qualité a été rejeté pour l'élément {1}., -Row {0}: UOM Conversion Factor is mandatory,Ligne {0} : Facteur de Conversion LDM est obligatoire, +Row {0}: UOM Conversion Factor is mandatory,Ligne {0} : Facteur de Conversion nomenclature est obligatoire, Row {0}: select the workstation against the operation {1},Ligne {0}: sélectionnez le poste de travail en fonction de l'opération {1}, Row {0}: {1} Serial numbers required for Item {2}. You have provided {3}.,Ligne {0}: {1} Numéros de série requis pour l'article {2}. Vous en avez fourni {3}., Row {0}: {1} must be greater than 0,Ligne {0}: {1} doit être supérieure à 0, @@ -2587,8 +2587,8 @@ See past quotations,Voir les citations passées, Select,Sélectionner, Select Alternate Item,Sélectionnez un autre élément, Select Attribute Values,Sélectionner les valeurs d'attribut, -Select BOM,Sélectionner LDM, -Select BOM and Qty for Production,Sélectionner la LDM et la Qté pour la Production, +Select BOM,Sélectionner une nomenclature, +Select BOM and Qty for Production,Sélectionner la nomenclature et la Qté pour la Production, "Select BOM, Qty and For Warehouse","Sélectionner une nomenclature, une quantité et un entrepôt", Select Batch,Sélectionnez le Lot, Select Batch Numbers,Sélectionnez les Numéros de Lot, @@ -2760,7 +2760,7 @@ Source and target warehouse cannot be same for row {0},L'entrepôt source et des Source and target warehouse must be different,Entrepôt source et destination doivent être différents, Source of Funds (Liabilities),Source des Fonds (Passif), Source warehouse is mandatory for row {0},Entrepôt source est obligatoire à la ligne {0}, -Specified BOM {0} does not exist for Item {1},La LDM {0} spécifiée n'existe pas pour l'Article {1}, +Specified BOM {0} does not exist for Item {1},La nomenclature {0} spécifiée n'existe pas pour l'Article {1}, Split,Fractionner, Split Batch,Lot Fractionné, Split Issue,Diviser le ticket, @@ -2888,11 +2888,11 @@ Supplies made to UIN holders,Fournitures faites aux titulaires de l'UIN, Supplies made to Unregistered Persons,Fournitures faites à des personnes non inscrites, Suppliies made to Composition Taxable Persons,Suppleies à des personnes assujetties à la composition, Supply Type,Type d'approvisionnement, -Support,Soutien, -Support Analytics,Analyse du Support, -Support Settings,Paramètres du Support, -Support Tickets,Billets de Support, -Support queries from customers.,Demande de support des clients, +Support,"Assistance/Support", +Support Analytics,Analyse de l'assistance, +Support Settings,Paramètres du module Assistance, +Support Tickets,Ticket d'assistance, +Support queries from customers.,Demande d'assistance des clients, Susceptible,Sensible, Sync has been temporarily disabled because maximum retries have been exceeded,La synchronisation a été temporairement désactivée car les tentatives maximales ont été dépassées, Syntax error in condition: {0},Erreur de syntaxe dans la condition: {0}, @@ -2965,7 +2965,7 @@ The name of the institute for which you are setting up this system.,Le nom de l' The name of your company for which you are setting up this system.,Le nom de l'entreprise pour laquelle vous configurez ce système., The number of shares and the share numbers are inconsistent,Le nombre d'actions dans les transactions est incohérent avec le nombre total d'actions, The payment gateway account in plan {0} is different from the payment gateway account in this payment request,Le compte passerelle de paiement dans le plan {0} est différent du compte passerelle de paiement dans cette requête de paiement., -The selected BOMs are not for the same item,Les LDMs sélectionnées ne sont pas pour le même article, +The selected BOMs are not for the same item,Les nomenclatures sélectionnées ne sont pas pour le même article, The selected item cannot have Batch,L’article sélectionné ne peut pas avoir de Lot, The seller and the buyer cannot be the same,Le vendeur et l'acheteur ne peuvent pas être les mêmes, The shareholder does not belong to this company,L'actionnaire n'appartient pas à cette société, @@ -3150,7 +3150,7 @@ Transporter Name,Nom du transporteur, Travel,Déplacement, Travel Expenses,Frais de Déplacement, Tree Type,Type d'Arbre, -Tree of Bill of Materials,Arbre des Listes de Matériaux, +Tree of Bill of Materials,Arbre des Nomenclatures, Tree of Item Groups.,Arbre de Groupes d’Articles ., Tree of Procedures,Arbre de procédures, Tree of Quality Procedures.,Arbre de la qualité des procédures., @@ -3305,7 +3305,7 @@ Wire Transfer,Virement, WooCommerce Products,Produits WooCommerce, Work In Progress,Travaux en cours, Work Order,Ordre de travail, -Work Order already created for all items with BOM,Ordre de travail déjà créé pour tous les articles avec une LDM, +Work Order already created for all items with BOM,Ordre de travail déjà créé pour tous les articles avec une nomenclature, Work Order cannot be raised against a Item Template,Un ordre de travail ne peut pas être créé pour un modèle d'article, Work Order has been {0},L'ordre de travail a été {0}, Work Order not created,Ordre de travail non créé, @@ -3326,7 +3326,7 @@ You are not authorized to add or update entries before {0},Vous n'êtes pas auto You are not authorized to approve leaves on Block Dates,Vous n'êtes pas autorisé à approuver les congés sur les Dates Bloquées, You are not authorized to set Frozen value,Vous n'êtes pas autorisé à définir des valeurs gelées, You are not present all day(s) between compensatory leave request days,Vous n'êtes pas présent(e) tous les jours vos demandes de congé compensatoire, -You can not change rate if BOM mentioned agianst any item,Vous ne pouvez pas modifier le taux si la LDM est mentionnée pour un article, +You can not change rate if BOM mentioned agianst any item,Vous ne pouvez pas modifier le taux si la nomenclature est mentionnée pour un article, You can not enter current voucher in 'Against Journal Entry' column,Vous ne pouvez pas entrer le bon actuel dans la colonne 'Pour l'Écriture de Journal', You can only have Plans with the same billing cycle in a Subscription,Vous ne pouvez avoir que des plans ayant le même cycle de facturation dans le même abonnement, You can only redeem max {0} points in this order.,Vous pouvez uniquement échanger un maximum de {0} points dans cet commande., @@ -5502,7 +5502,7 @@ Blanket Order,Commande avec limites, Blanket Order Rate,Prix unitaire de commande avec limites, Returned Qty,Qté Retournée, Purchase Order Item Supplied,Article Fourni du Bon de Commande, -BOM Detail No,N° de Détail LDM, +BOM Detail No,N° de Détail de la nomenclature, Stock Uom,UDM du Stock, Raw Material Item Code,Code d’Article de Matière Première, Supplied Qty,Qté Fournie, @@ -5600,7 +5600,6 @@ Call Log,Journal d'appel, Received By,Reçu par, Caller Information,Informations sur l'appelant, Contact Name,Nom du Contact, -Lead ,Conduire, Lead Name,Nom du Prospect, Ringing,Sonnerie, Missed,Manqué, @@ -7183,7 +7182,7 @@ Blanket Order Item,Article de commande avec limites, Ordered Quantity,Quantité Commandée, Item to be manufactured or repacked,Article à produire ou à réemballer, Quantity of item obtained after manufacturing / repacking from given quantities of raw materials,Quantité d'article obtenue après production / reconditionnement des quantités données de matières premières, -Set rate of sub-assembly item based on BOM,Définir le prix des articles de sous-assemblage en fonction de la LDM, +Set rate of sub-assembly item based on BOM,Définir le prix des articles de sous-assemblage en fonction de la nomenclature, Allow Alternative Item,Autoriser un article alternatif, Item UOM,UDM de l'Article, Conversion Rate,Taux de Conversion, @@ -7214,33 +7213,33 @@ Website Specifications,Spécifications du Site Web, Show Items,Afficher les Articles, Show Operations,Afficher Opérations, Website Description,Description du Site Web, -BOM Explosion Item,Article Eclaté LDM, +BOM Explosion Item,Article Eclaté en nomenclature, Qty Consumed Per Unit,Qté Consommée Par Unité, Include Item In Manufacturing,Inclure l'article dans la fabrication, -BOM Item,Article LDM, +BOM Item,Article de la nomenclature, Item operation,Opération de l'article, Rate & Amount,Taux et Montant, Basic Rate (Company Currency),Taux de Base (Devise de la Société ), Scrap %,% de Rebut, Original Item,Article original, -BOM Operation,Opération LDM, +BOM Operation,Opération de la nomenclature (gamme), Operation Time ,Durée de l'opération, In minutes,En minutes, Batch Size,Taille du lot, Base Hour Rate(Company Currency),Taux Horaire de Base (Devise de la Société), Operating Cost(Company Currency),Coût d'Exploitation (Devise Société), -BOM Scrap Item,Article Mis au Rebut LDM, +BOM Scrap Item,Article Mis au Rebut dans la nomenclature, Basic Amount (Company Currency),Montant de Base (Devise de la Société), -BOM Update Tool,Outil de mise à jour de LDM, -"Replace a particular BOM in all other BOMs where it is used. It will replace the old BOM link, update cost and regenerate ""BOM Explosion Item"" table as per new BOM.\nIt also updates latest price in all the BOMs.","Remplacez une LDM particulière dans toutes les LDM où elles est utilisée. Cela remplacera le lien vers l'ancienne LDM, mettra à jour les coûts et régénérera le tableau ""Article Explosé de LDM"" selon la nouvelle LDM. Cela mettra également à jour les prix les plus récents dans toutes les LDMs.", -Replace BOM,Remplacer la LDM, -Current BOM,LDM Actuelle, -The BOM which will be replaced,La LDM qui sera remplacée, -The new BOM after replacement,La nouvelle LDM après remplacement, +BOM Update Tool,Outil de mise à jour des Nomenclatures, +"Replace a particular BOM in all other BOMs where it is used. It will replace the old BOM link, update cost and regenerate ""BOM Explosion Item"" table as per new BOM.\nIt also updates latest price in all the BOMs.","Remplacez une nomenclature particulière dans toutes les nomenclatures où elles est utilisée. Cela remplacera le lien vers l'ancienne nomenclature, mettra à jour les coûts et régénérera le tableau ""Article Explosé de nomenclature"" selon la nouvelle nomenclature. Cela mettra également à jour les prix les plus récents dans toutes les nomenclatures.", +Replace BOM,Remplacer la nomenclature, +Current BOM,nomenclature Actuelle, +The BOM which will be replaced,La nomenclature qui sera remplacée, +The new BOM after replacement,La nouvelle nomenclature après remplacement, Replace,Remplacer, -Update latest price in all BOMs,Mettre à jour le prix le plus récent dans toutes les LDMs, -BOM Website Item,Article de LDM du Site Internet, -BOM Website Operation,Opération de LDM du Site Internet, +Update latest price in all BOMs,Mettre à jour le prix le plus récent dans toutes les nomenclatures, +BOM Website Item,Article de nomenclature du Site Internet, +BOM Website Operation,Opération de nomenclature du Site Internet, Operation Time,Heure de l'Opération, PO-JOB.#####,PO-JOB. #####, Timing Detail,Détail du timing, @@ -7272,7 +7271,7 @@ Default Scrap Warehouse,Entrepôt de rebut par défaut, Overproduction Percentage For Sales Order,Pourcentage de surproduction pour les commandes client, Overproduction Percentage For Work Order,Pourcentage de surproduction pour les ordres de travail, Other Settings,Autres Paramètres, -Update BOM Cost Automatically,Mettre à jour automatiquement le coût de la LDM, +Update BOM Cost Automatically,Mettre à jour automatiquement le coût de la nomenclature, Material Request Plan Item,Article du plan de demande de matériel, Material Request Type,Type de Demande de Matériel, Material Issue,Sortie de Matériel, @@ -7312,7 +7311,7 @@ MFG-WO-.YYYY.-,MFG-WO-.YYYY.-, Item To Manufacture,Article à produire, Material Transferred for Manufacturing,Matériel Transféré pour la Production, Manufactured Qty,Qté Produite, -Use Multi-Level BOM,Utiliser LDM à Plusieurs Niveaux, +Use Multi-Level BOM,Utiliser les nomenclatures à plusieurs niveaux, Plan material for sub-assemblies,Plan de matériaux pour les sous-ensembles, Skip Material Transfer to WIP Warehouse,Ignorer le transfert de matériel vers l'entrepôt WIP, Check if material transfer entry is not required,Vérifiez si une un transfert de matériel n'est pas requis, @@ -7685,7 +7684,7 @@ Collected Amount,Montant collecté, Expected Amount,Montant prévu, POS Closing Voucher Invoices,Factures du bon de clôture du PDV, Quantity of Items,Quantité d'articles, -"Aggregate group of **Items** into another **Item**. This is useful if you are bundling a certain **Items** into a package and you maintain stock of the packed **Items** and not the aggregate **Item**. \n\nThe package **Item** will have ""Is Stock Item"" as ""No"" and ""Is Sales Item"" as ""Yes"".\n\nFor Example: If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.\n\nNote: BOM = Bill of Materials","Regroupement d' **Articles** dans un autre **Article**. Ceci est utile si vous regroupez certains **Articles** dans un lot et que vous maintenez l'inventaire des **Articles** du lot et non de l'**Article** composé. L'**Article** composé aura ""Article En Stock"" à ""Non"" et ""Article À Vendre"" à ""Oui"". Exemple : Si vous vendez des Ordinateurs Portables et Sacs à Dos séparément et qu'il y a un prix spécial si le client achète les deux, alors l'Ordinateur Portable + le Sac à Dos sera un nouveau Produit Groupé. Remarque: LDM = Liste\nDes Matériaux", +"Aggregate group of **Items** into another **Item**. This is useful if you are bundling a certain **Items** into a package and you maintain stock of the packed **Items** and not the aggregate **Item**. \n\nThe package **Item** will have ""Is Stock Item"" as ""No"" and ""Is Sales Item"" as ""Yes"".\n\nFor Example: If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.\n\nNote: BOM = Bill of Materials","Regroupement d' **Articles** dans un autre **Article**. Ceci est utile si vous regroupez certains **Articles** dans un lot et que vous maintenez l'inventaire des **Articles** du lot et non de l'**Article** composé. L'**Article** composé aura ""Article En Stock"" à ""Non"" et ""Article À Vendre"" à ""Oui"". Exemple : Si vous vendez des Ordinateurs Portables et Sacs à Dos séparément et qu'il y a un prix spécial si le client achète les deux, alors l'Ordinateur Portable + le Sac à Dos sera un nouveau Produit Groupé.", Parent Item,Article Parent, List items that form the package.,Liste des articles qui composent le paquet., SAL-QTN-.YYYY.-,SAL-QTN-. AAAA.-, @@ -8089,7 +8088,7 @@ Customer Items,Articles du clients, Inspection Criteria,Critères d'Inspection, Inspection Required before Purchase,Inspection Requise avant Achat, Inspection Required before Delivery,Inspection Requise avant Livraison, -Default BOM,LDM par Défaut, +Default BOM,Nomenclature par Défaut, Supply Raw Materials for Purchase,Fournir les Matières Premières pour l'Achat, If subcontracted to a vendor,Si sous-traité à un fournisseur, Customer Code,Code Client, @@ -8295,7 +8294,7 @@ Delivery Note No,Bon de Livraison N°, Sales Invoice No,N° de la Facture de Vente, Purchase Receipt No,N° du Reçu d'Achat, Inspection Required,Inspection obligatoire, -From BOM,De LDM, +From BOM,Depuis la nomenclature, For Quantity,Pour la Quantité, As per Stock UOM,Selon UDM du Stock, Including items for sub assemblies,Incluant les articles pour des sous-ensembles, @@ -8316,7 +8315,7 @@ Basic Rate (as per Stock UOM),Taux de base (comme l’UDM du Stock), Basic Amount,Montant de Base, Additional Cost,Frais Supplémentaire, Serial No / Batch,N° de Série / Lot, -BOM No. for a Finished Good Item,N° d’Article Produit Fini LDM, +BOM No. for a Finished Good Item,N° de nomenclature pour un d’Article (Produit Fini), Material Request used to make this Stock Entry,Demande de Matériel utilisée pour réaliser cette Écriture de Stock, Subcontracted Item,Article sous-traité, Against Stock Entry,Contre entrée de stock, @@ -8456,9 +8455,9 @@ Bank Remittance,Virement bancaire, Batch Item Expiry Status,Statut d'Expiration d'Article du Lot, Batch-Wise Balance History,Historique de Balance des Lots, BOM Explorer,Explorateur de nomenclature, -BOM Search,Recherche LDM, -BOM Stock Calculated,Stock calculé par liste de matériaux (LDM), -BOM Variance Report,Rapport de variance par liste de matériaux (LDM), +BOM Search,Recherche nomenclature, +BOM Stock Calculated,Stock calculé par nomenclature, +BOM Variance Report,Rapport de variance par nomenclature, Campaign Efficiency,Efficacité des Campagnes, Cash Flow,Flux de Trésorerie, Completed Work Orders,Ordres de travail terminés, @@ -9873,3 +9872,7 @@ Convert Item Description to Clean HTML in Transactions,Convertir les description Have Default Naming Series for Batch ID?,Nom de série 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" Unit Of Measure (UOM),Unité de mesure (UDM), +Allowed Items,Articles autorisés +Party Specific Item,Restriction d'article disponible +Restrict Items Based On,Type de critére de restriction +Based On Value,critére de restriction From 2de2491e1737a9935183ca8f1db953f77f6c1185 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 27 May 2022 20:33:14 +0530 Subject: [PATCH 093/192] perf: `get_next_higher_level_boms` - Separate getting dependants and checking if they are valid (loop within loop led to redundant processing that slowed down function) - Adding to above, the same dependant(parent) was repeatedly processed as many children shared it. Expensive. - Use a parent-child map similar to child-parent map to check if all children are resolved - `map.get()` reduced time: 10 mins -> 0.9s~1 second (as compared to `get_cached_doc` or query) - Total time: 17 seconds to process 6599 leaf boms and 4.2L parent boms - Previous Total time: >10 mins (I terminated it due to not wanting to waste time XD) --- .../bom_update_log/bom_updation_utils.py | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py index 1ec15f0d3a..790a79b333 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py @@ -159,21 +159,29 @@ def process_if_level_is_complete( def get_next_higher_level_boms( child_boms: Dict[str, bool], processed_boms: Dict[str, bool] ) -> List[str]: - "Generate immediate higher level dependants with no unresolved dependencies." + "Generate immediate higher level dependants with no unresolved dependencies (children)." - def _all_children_are_processed(parent): - bom_doc = frappe.get_cached_doc("BOM", parent) - return all(processed_boms.get(row.bom_no) for row in bom_doc.items if row.bom_no) + def _all_children_are_processed(parent_bom): + child_boms = dependency_map.get(parent_bom) + return all(processed_boms.get(bom) for bom in child_boms) - dependants_map = _generate_dependants_map() - dependants = set() + dependants_map, dependency_map = _generate_dependence_map() + + dependants = [] for bom in child_boms: + # generate list of immediate dependants parents = dependants_map.get(bom) or [] - for parent in parents: - if _all_children_are_processed(parent): - dependants.add(parent) + dependants.extend(parents) - return list(dependants) + dependants = set(dependants) # remove duplicates + resolved_dependants = set() + + # consider only if children are all resolved + for parent_bom in dependants: + if _all_children_are_processed(parent_bom): + resolved_dependants.add(parent_bom) + + return list(resolved_dependants) def get_leaf_boms() -> List[str]: @@ -187,17 +195,19 @@ def get_leaf_boms() -> List[str]: ) -def _generate_dependants_map() -> defaultdict: +def _generate_dependence_map() -> defaultdict: """ - Generate map such as: { BOM-1: [Dependant-BOM-1, Dependant-BOM-2, ..] }. + Generate maps such as: { BOM-1: [Dependant-BOM-1, Dependant-BOM-2, ..] }. Here BOM-1 is the leaf/lower level node/dependency. The list contains one level higher nodes/dependants that depend on BOM-1. + + Generate and return the reverse as well. """ bom = frappe.qb.DocType("BOM") bom_item = frappe.qb.DocType("BOM Item") - bom_parents = ( + bom_items = ( frappe.qb.from_(bom_item) .join(bom) .on(bom_item.parent == bom.name) @@ -212,10 +222,12 @@ def _generate_dependants_map() -> defaultdict: ).run(as_dict=True) child_parent_map = defaultdict(list) - for bom in bom_parents: - child_parent_map[bom.bom_no].append(bom.parent) + parent_child_map = defaultdict(list) + for row in bom_items: + child_parent_map[row.bom_no].append(row.parent) + parent_child_map[row.parent].append(row.bom_no) - return child_parent_map + return child_parent_map, parent_child_map def set_values_in_log(log_name: str, values: Dict[str, Any], commit: bool = False) -> None: From 978ba5238fae64fe0e348091d42c3210b999df02 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 27 May 2022 21:59:59 +0530 Subject: [PATCH 094/192] fix: Safe cast `row.rate` (in case of faulty exploded items, edge case but oh well) --- erpnext/manufacturing/doctype/bom/bom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 15048ec990..7055efac28 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -699,7 +699,7 @@ class BOM(WebsiteGenerator): for row in self.get("exploded_items"): old_rate = flt(row.rate) row.rate = rm_rate_map.get(row.item_code) - row.amount = flt(row.stock_qty) * row.rate + row.amount = flt(row.stock_qty) * flt(row.rate) if old_rate != row.rate: # Only db_update if changed From 933434c3eab5fc42c1ce92d2ada7c1bffeea282a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 29 May 2022 22:09:32 +0530 Subject: [PATCH 095/192] chore: format --- .../quality_inspection_summary/quality_inspection_summary.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py index c324172372..de96a6c032 100644 --- a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py +++ b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py @@ -34,7 +34,6 @@ def get_data(filters): if filters.get(field): query_filters[field] = ("in", filters.get(field)) - query_filters["report_date"] = ["between", [filters.get("from_date"), filters.get("to_date")]] return frappe.get_all( From 85b48fcdb9f0afb79d958c006925088cd1928c21 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Sun, 29 May 2022 12:49:09 -0400 Subject: [PATCH 096/192] fix: barcode scan resolve after model is updated (#31058) * fix: resolve row after model is updated. * fix: wait for all fields in the model to be updated. * fix: sider * pref: clear scanned code after capturing value * fix: use frappe.run_serially --- erpnext/public/js/utils/barcode_scanner.js | 88 +++++++++++----------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js index d378118564..eea91ef5fe 100644 --- a/erpnext/public/js/utils/barcode_scanner.js +++ b/erpnext/public/js/utils/barcode_scanner.js @@ -35,6 +35,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { let me = this; const input = this.scan_barcode_field.value; + this.scan_barcode_field.set_value(""); if (!input) { return; } @@ -55,51 +56,51 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { return; } - const row = me.update_table(data); - if (row) { - resolve(row); - } - else { - reject(); - } + me.update_table(data).then(row => { + row ? resolve(row) : reject(); + }); }); }); } update_table(data) { - let cur_grid = this.frm.fields_dict[this.items_table_name].grid; + return new Promise(resolve => { + let cur_grid = this.frm.fields_dict[this.items_table_name].grid; - const {item_code, barcode, batch_no, serial_no} = data; + const {item_code, barcode, batch_no, serial_no} = data; - let row = this.get_row_to_modify_on_scan(item_code, batch_no); + let row = this.get_row_to_modify_on_scan(item_code, batch_no); - if (!row) { - if (this.dont_allow_new_row) { - this.show_alert(__("Maximum quantity scanned for item {0}.", [item_code]), "red"); + if (!row) { + if (this.dont_allow_new_row) { + this.show_alert(__("Maximum quantity scanned for item {0}.", [item_code]), "red"); + this.clean_up(); + return; + } + + // add new row if new item/batch is scanned + row = frappe.model.add_child(this.frm.doc, cur_grid.doctype, this.items_table_name); + // trigger any row add triggers defined on child table. + this.frm.script_manager.trigger(`${this.items_table_name}_add`, row.doctype, row.name); + } + + if (this.is_duplicate_serial_no(row, serial_no)) { this.clean_up(); return; } - // add new row if new item/batch is scanned - row = frappe.model.add_child(this.frm.doc, cur_grid.doctype, this.items_table_name); - // trigger any row add triggers defined on child table. - this.frm.script_manager.trigger(`${this.items_table_name}_add`, row.doctype, row.name); - } - - if (this.is_duplicate_serial_no(row, serial_no)) { - this.clean_up(); - return; - } - - this.set_selector_trigger_flag(row, data); - this.set_item(row, item_code).then(qty => { - this.show_scan_message(row.idx, row.item_code, qty); + frappe.run_serially([ + () => this.set_selector_trigger_flag(row, data), + () => this.set_item(row, item_code).then(qty => { + this.show_scan_message(row.idx, row.item_code, qty); + }), + () => this.set_serial_no(row, serial_no), + () => this.set_batch_no(row, batch_no), + () => this.set_barcode(row, barcode), + () => this.clean_up(), + () => resolve(row) + ]); }); - this.set_serial_no(row, serial_no); - this.set_batch_no(row, batch_no); - this.set_barcode(row, barcode); - this.clean_up(); - return row; } // batch and serial selector is reduandant when all info can be added by scan @@ -117,25 +118,24 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { set_item(row, item_code) { return new Promise(resolve => { - const increment = (value = 1) => { + const increment = async (value = 1) => { const item_data = {item_code: item_code}; item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value); - frappe.model.set_value(row.doctype, row.name, item_data); + await frappe.model.set_value(row.doctype, row.name, item_data); + return value; }; if (this.prompt_qty) { frappe.prompt(__("Please enter quantity for item {0}", [item_code]), ({value}) => { - increment(value); - resolve(value); + increment(value).then((value) => resolve(value)); }); } else { - increment(); - resolve(); + increment().then((value) => resolve(value)); } }); } - set_serial_no(row, serial_no) { + async set_serial_no(row, serial_no) { if (serial_no && frappe.meta.has_field(row.doctype, this.serial_no_field)) { const existing_serial_nos = row[this.serial_no_field]; let new_serial_nos = ""; @@ -145,19 +145,19 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { } else { new_serial_nos = serial_no; } - frappe.model.set_value(row.doctype, row.name, this.serial_no_field, new_serial_nos); + await frappe.model.set_value(row.doctype, row.name, this.serial_no_field, new_serial_nos); } } - set_batch_no(row, batch_no) { + async set_batch_no(row, batch_no) { if (batch_no && frappe.meta.has_field(row.doctype, this.batch_no_field)) { - frappe.model.set_value(row.doctype, row.name, this.batch_no_field, batch_no); + await frappe.model.set_value(row.doctype, row.name, this.batch_no_field, batch_no); } } - set_barcode(row, barcode) { + async set_barcode(row, barcode) { if (barcode && frappe.meta.has_field(row.doctype, this.barcode_field)) { - frappe.model.set_value(row.doctype, row.name, this.barcode_field, barcode); + await frappe.model.set_value(row.doctype, row.name, this.barcode_field, barcode); } } From b170cec2fe84f4fc8d0908979194a61f40df0f74 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 29 May 2022 14:32:32 +0530 Subject: [PATCH 097/192] fix(ux): "New Version" button BOM "duplicate" technically creates a new version but that's not intuitive at all. --- erpnext/manufacturing/doctype/bom/bom.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 3d96f9c9c7..d74379881c 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -93,6 +93,11 @@ frappe.ui.form.on("BOM", { }); } + frm.add_custom_button(__("New Version"), function() { + let new_bom = frappe.model.copy_doc(frm.doc); + frappe.set_route("Form", "BOM", new_bom.name); + }); + if(frm.doc.docstatus==1) { frm.add_custom_button(__("Work Order"), function() { frm.trigger("make_work_order"); From d224bf1d3450ab0d63627f19b7f707074d8a1716 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 30 May 2022 10:33:58 +0530 Subject: [PATCH 098/192] fix: only erase BOM when do_not_explode is set --- erpnext/manufacturing/doctype/bom/bom.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 220ce1dbd8..3560c32166 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -251,9 +251,8 @@ class BOM(WebsiteGenerator): for item in self.get("items"): self.validate_bom_currency(item) - item.bom_no = "" - if not item.do_not_explode: - item.bom_no = item.bom_no + if item.do_not_explode: + item.bom_no = "" ret = self.get_bom_material_detail( { From 954dac88a85f7760225348d5655c65c71f89825f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 29 May 2022 14:11:45 +0530 Subject: [PATCH 099/192] fix: allow non-explosive recrusive BOMs Recursion should be allowed as long as child item is not "exploded" further by a BOM. --- erpnext/manufacturing/doctype/bom/bom.py | 44 +++++++++---------- erpnext/manufacturing/doctype/bom/test_bom.py | 33 ++++++-------- 2 files changed, 33 insertions(+), 44 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 3560c32166..6376359a70 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -22,6 +22,10 @@ from erpnext.stock.get_item_details import get_conversion_factor, get_price_list form_grid_templates = {"items": "templates/form_grid/item_grid.html"} +class BOMRecursionError(frappe.ValidationError): + pass + + class BOMTree: """Full tree representation of a BOM""" @@ -554,35 +558,27 @@ class BOM(WebsiteGenerator): """Check whether recursion occurs in any bom""" def _throw_error(bom_name): - frappe.throw(_("BOM recursion: {0} cannot be parent or child of {0}").format(bom_name)) + frappe.throw( + _("BOM recursion: {1} cannot be parent or child of {0}").format(self.name, bom_name), + exc=BOMRecursionError, + ) bom_list = self.traverse_tree() - child_items = ( - frappe.get_all( - "BOM Item", - fields=["bom_no", "item_code"], - filters={"parent": ("in", bom_list), "parenttype": "BOM"}, - ) - or [] + child_items = frappe.get_all( + "BOM Item", + fields=["bom_no", "item_code"], + filters={"parent": ("in", bom_list), "parenttype": "BOM"}, ) - child_bom = {d.bom_no for d in child_items} - child_items_codes = {d.item_code for d in child_items} + for item in child_items: + if self.name == item.bom_no: + _throw_error(self.name) + if self.item == item.item_code and item.bom_no: + # Same item but with different BOM should not be allowed. + # Same item can appear recursively once as long as it doesn't have BOM. + _throw_error(item.bom_no) - if self.name in child_bom: - _throw_error(self.name) - - if self.item in child_items_codes: - _throw_error(self.item) - - bom_nos = ( - frappe.get_all( - "BOM Item", fields=["parent"], filters={"bom_no": self.name, "parenttype": "BOM"} - ) - or [] - ) - - if self.name in {d.parent for d in bom_nos}: + if self.name in {d.bom_no for d in self.items}: _throw_error(self.name) def traverse_tree(self, bom_list=None): diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 62fc0724e0..f235e449a3 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -10,7 +10,7 @@ from frappe.tests.utils import FrappeTestCase from frappe.utils import cstr, flt from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order -from erpnext.manufacturing.doctype.bom.bom import item_query, make_variant_bom +from erpnext.manufacturing.doctype.bom.bom import BOMRecursionError, item_query, make_variant_bom from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( @@ -324,43 +324,36 @@ class TestBOM(FrappeTestCase): def test_bom_recursion_1st_level(self): """BOM should not allow BOM item again in child""" - item_code = "_Test BOM Recursion" - make_item(item_code, {"is_stock_item": 1}) + item_code = make_item(properties={"is_stock_item": 1}).name bom = frappe.new_doc("BOM") bom.item = item_code bom.append("items", frappe._dict(item_code=item_code)) - with self.assertRaises(frappe.ValidationError) as err: + bom.save() + with self.assertRaises(BOMRecursionError): + bom.items[0].bom_no = bom.name bom.save() - self.assertTrue("recursion" in str(err.exception).lower()) - frappe.delete_doc("BOM", bom.name, ignore_missing=True) - def test_bom_recursion_transitive(self): - item1 = "_Test BOM Recursion" - item2 = "_Test BOM Recursion 2" - make_item(item1, {"is_stock_item": 1}) - make_item(item2, {"is_stock_item": 1}) + item1 = make_item(properties={"is_stock_item": 1}).name + item2 = make_item(properties={"is_stock_item": 1}).name bom1 = frappe.new_doc("BOM") bom1.item = item1 bom1.append("items", frappe._dict(item_code=item2)) bom1.save() - bom1.submit() bom2 = frappe.new_doc("BOM") bom2.item = item2 bom2.append("items", frappe._dict(item_code=item1)) + bom2.save() - with self.assertRaises(frappe.ValidationError) as err: + bom2.items[0].bom_no = bom1.name + bom1.items[0].bom_no = bom2.name + + with self.assertRaises(BOMRecursionError): + bom1.save() bom2.save() - bom2.submit() - - self.assertTrue("recursion" in str(err.exception).lower()) - - bom1.cancel() - frappe.delete_doc("BOM", bom1.name, ignore_missing=True, force=True) - frappe.delete_doc("BOM", bom2.name, ignore_missing=True, force=True) def test_bom_with_process_loss_item(self): fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items() From c02598a51bb38817401e62e305faf82da857ff6d Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 30 May 2022 11:12:17 +0530 Subject: [PATCH 100/192] chore: remove framework tests from erpnext Similar tests exist in FW and this is failing because someone updated the translations --- erpnext/tests/test_search.py | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 erpnext/tests/test_search.py diff --git a/erpnext/tests/test_search.py b/erpnext/tests/test_search.py deleted file mode 100644 index 3685828667..0000000000 --- a/erpnext/tests/test_search.py +++ /dev/null @@ -1,19 +0,0 @@ -import unittest - -import frappe -from frappe.contacts.address_and_contact import filter_dynamic_link_doctypes - - -class TestSearch(unittest.TestCase): - # Search for the word "cond", part of the word "conduire" (Lead) in french. - def test_contact_search_in_foreign_language(self): - try: - frappe.local.lang_full_dict = None # reset cached translations - frappe.local.lang = "fr" - output = filter_dynamic_link_doctypes( - "DocType", "cond", "name", 0, 20, {"fieldtype": "HTML", "fieldname": "contact_html"} - ) - result = [["found" for x in y if x == "Lead"] for y in output] - self.assertTrue(["found"] in result) - finally: - frappe.local.lang = "en" From de5515799732cbfb06a9e05ee3951f180a34e841 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 30 May 2022 12:47:26 +0530 Subject: [PATCH 101/192] chore: rename method `get_salary_component_account` method to `set` - since it doesn't return any value --- .../payroll/doctype/payroll_entry/test_payroll_entry.py | 8 ++++---- erpnext/payroll/doctype/salary_slip/test_salary_slip.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index fda0fcf8be..3fffeb8966 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -22,10 +22,10 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_ from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_end_date, get_start_end_dates from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( create_account, - get_salary_component_account, make_deduction_salary_component, make_earning_salary_component, make_employee_salary_slip, + set_salary_component_account, ) from erpnext.payroll.doctype.salary_structure.test_salary_structure import ( create_salary_structure_assignment, @@ -66,7 +66,7 @@ class TestPayrollEntry(unittest.TestCase): if not frappe.db.get_value( "Salary Component Account", {"parent": data.name, "company": company}, "name" ): - get_salary_component_account(data.name) + set_salary_component_account(data.name) employee = frappe.db.get_value("Employee", {"company": company}) company_doc = frappe.get_doc("Company", company) @@ -95,7 +95,7 @@ class TestPayrollEntry(unittest.TestCase): if not frappe.db.get_value( "Salary Component Account", {"parent": data.name, "company": company}, "name" ): - get_salary_component_account(data.name) + set_salary_component_account(data.name) company_doc = frappe.get_doc("Company", company) salary_structure = make_salary_structure( @@ -148,7 +148,7 @@ class TestPayrollEntry(unittest.TestCase): if not frappe.db.get_value( "Salary Component Account", {"parent": data.name, "company": "_Test Company"}, "name" ): - get_salary_component_account(data.name) + set_salary_component_account(data.name) if not frappe.db.exists("Department", "cc - _TC"): frappe.get_doc( diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 1bc3741922..91d335274e 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -1046,10 +1046,10 @@ def make_salary_component(salary_components, test_tax, company_list=None): doc.update(salary_component) doc.insert() - get_salary_component_account(doc, company_list) + set_salary_component_account(doc, company_list) -def get_salary_component_account(sal_comp, company_list=None): +def set_salary_component_account(sal_comp, company_list=None): company = erpnext.get_default_company() if company_list and company not in company_list: From 08bf0baaae3dad9fc74f15b0c42553b83f82e95d Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 30 May 2022 14:50:50 +0530 Subject: [PATCH 102/192] chore!: remove unused bill no & date from purchase receipt (#31163) --- .../purchase_receipt/purchase_receipt.json | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 983b62a09a..923ceb36cd 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -108,8 +108,6 @@ "terms_section_break", "tc_name", "terms", - "bill_no", - "bill_date", "more_info", "status", "amended_from", @@ -867,24 +865,6 @@ "oldfieldname": "terms", "oldfieldtype": "Text Editor" }, - { - "fieldname": "bill_no", - "fieldtype": "Data", - "hidden": 1, - "label": "Bill No", - "oldfieldname": "bill_no", - "oldfieldtype": "Data", - "print_hide": 1 - }, - { - "fieldname": "bill_date", - "fieldtype": "Date", - "hidden": 1, - "label": "Bill Date", - "oldfieldname": "bill_date", - "oldfieldtype": "Date", - "print_hide": 1 - }, { "collapsible": 1, "fieldname": "more_info", @@ -1168,7 +1148,7 @@ "idx": 261, "is_submittable": 1, "links": [], - "modified": "2022-04-26 13:41:32.625197", + "modified": "2022-05-27 15:59:18.550583", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", From 42f6bca9354d07855573235ab8508c49e5ba9cac Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 30 May 2022 16:04:07 +0530 Subject: [PATCH 103/192] fix: reset Error Message on successful operation and fix status update on submit/cancel --- .../doctype/payroll_entry/payroll_entry.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 86be813b91..266621d4d1 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -44,7 +44,7 @@ class PayrollEntry(Document): self.set_status() def on_submit(self): - self.set_status(update=True) + self.set_status(update=True, status="Submitted") self.create_salary_slips() def before_submit(self): @@ -90,7 +90,8 @@ class PayrollEntry(Document): ) self.db_set("salary_slips_created", 0) self.db_set("salary_slips_submitted", 0) - self.set_status(update=True) + self.set_status(update=True, status="Cancelled") + self.db_set("error_message", "") def get_emp_list(self): """ @@ -187,7 +188,7 @@ class PayrollEntry(Document): "currency": self.currency, } ) - if len(employees) > 30: + if len(employees) > 30 or frappe.flags.enqueue_payroll_entry: self.db_set("status", "Queued") frappe.enqueue( create_salary_slips_for_employees, @@ -230,14 +231,14 @@ class PayrollEntry(Document): @frappe.whitelist() def submit_salary_slips(self): self.check_permission("write") - ss_list = self.get_sal_slip_list(ss_status=0) - if len(ss_list) > 30: + salary_slips = self.get_sal_slip_list(ss_status=0) + if len(salary_slips) > 30 or frappe.flags.enqueue_payroll_entry: self.db_set("status", "Queued") frappe.enqueue( submit_salary_slips_for_employees, timeout=600, payroll_entry=self, - salary_slips=ss_list, + salary_slips=salary_slips, publish_progress=False, ) frappe.msgprint( @@ -246,7 +247,7 @@ class PayrollEntry(Document): indicator="blue", ) else: - submit_salary_slips_for_employees(self, ss_list, publish_progress=False) + submit_salary_slips_for_employees(self, salary_slips, publish_progress=False) def email_salary_slip(self, submitted_ss): if frappe.db.get_single_value("Payroll Settings", "email_salary_slip_to_employee"): @@ -857,7 +858,7 @@ def create_salary_slips_for_employees(employees, args, publish_progress=True): title=_("Creating Salary Slips..."), ) - payroll_entry.db_set({"status": "Submitted", "salary_slips_created": 1}) + payroll_entry.db_set({"status": "Submitted", "salary_slips_created": 1, "error_message": ""}) if salary_slips_exist_for: frappe.msgprint( @@ -873,7 +874,7 @@ def create_salary_slips_for_employees(employees, args, publish_progress=True): log_payroll_failure("creation", payroll_entry, e) finally: - frappe.db.commit() + frappe.db.commit() # nosemgrep frappe.publish_realtime("completed_salary_slip_creation") @@ -937,7 +938,7 @@ def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progr if submitted: payroll_entry.make_accrual_jv_entry() payroll_entry.email_salary_slip(submitted) - payroll_entry.db_set({"salary_slips_submitted": 1, "status": "Submitted"}) + payroll_entry.db_set({"salary_slips_submitted": 1, "status": "Submitted", "error_message": ""}) show_payroll_submission_status(submitted, not_submitted, salary_slip) @@ -946,7 +947,7 @@ def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progr log_payroll_failure("submission", payroll_entry, e) finally: - frappe.db.commit() + frappe.db.commit() # nosemgrep frappe.publish_realtime("completed_salary_slip_submission") frappe.flags.via_payroll_entry = False From 78c39e947bf747032c935dab6d76092836fa0e61 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 30 May 2022 16:07:19 +0530 Subject: [PATCH 104/192] test: Salary Slip operations queuing, failure, and payroll entry status - fix multicurrency test, remove redundant doc creation --- .../payroll_entry/test_payroll_entry.py | 172 ++++++++++++++---- .../salary_structure/test_salary_structure.py | 3 - 2 files changed, 137 insertions(+), 38 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index 3fffeb8966..5c68bd35ef 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -5,6 +5,7 @@ import unittest import frappe from dateutil.relativedelta import relativedelta +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_months import erpnext @@ -35,14 +36,12 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import ( test_dependencies = ["Holiday List"] -class TestPayrollEntry(unittest.TestCase): - @classmethod - def setUpClass(cls): +class TestPayrollEntry(FrappeTestCase): + def setUp(self): frappe.db.set_value( "Company", erpnext.get_default_company(), "default_holiday_list", "_Test Holiday List" ) - def setUp(self): for dt in [ "Salary Slip", "Salary Component", @@ -88,40 +87,40 @@ class TestPayrollEntry(unittest.TestCase): currency=company_doc.default_currency, ) - def test_multi_currency_payroll_entry(self): # pylint: disable=no-self-use - company = erpnext.get_default_company() - employee = make_employee("test_muti_currency_employee@payroll.com", company=company) + def test_multi_currency_payroll_entry(self): + company = frappe.get_doc("Company", "_Test Company") + employee = make_employee( + "test_muti_currency_employee@payroll.com", company=company.name, department="Accounts - _TC" + ) + for data in frappe.get_all("Salary Component", fields=["name"]): if not frappe.db.get_value( - "Salary Component Account", {"parent": data.name, "company": company}, "name" + "Salary Component Account", {"parent": data.name, "company": company.name}, "name" ): set_salary_component_account(data.name) - company_doc = frappe.get_doc("Company", company) - salary_structure = make_salary_structure( - "_Test Multi Currency Salary Structure", "Monthly", company=company, currency="USD" - ) - create_salary_structure_assignment( - employee, salary_structure.name, company=company, currency="USD" - ) - frappe.db.sql( - """delete from `tabSalary Slip` where employee=%s""", - (frappe.db.get_value("Employee", {"user_id": "test_muti_currency_employee@payroll.com"})), - ) - salary_slip = get_salary_slip( - "test_muti_currency_employee@payroll.com", "Monthly", "_Test Multi Currency Salary Structure" + salary_struct = make_salary_structure( + "_Test Multi Currency Salary Structure", + "Monthly", + employee, + currency="USD", + company=company.name, ) + + frappe.db.delete("Salary Slip", {"employee": employee}) dates = get_start_end_dates("Monthly", nowdate()) payroll_entry = make_payroll_entry( start_date=dates.start_date, end_date=dates.end_date, - payable_account=company_doc.default_payroll_payable_account, + payable_account=company.default_payroll_payable_account, currency="USD", exchange_rate=70, + company=company.name, ) payroll_entry.make_payment_entry() - salary_slip.load_from_db() + salary_slip = frappe.db.get_value("Salary Slip", {"payroll_entry": payroll_entry.name}) + salary_slip = frappe.get_doc("Salary Slip", salary_slip) payroll_je = salary_slip.journal_entry if payroll_je: @@ -143,7 +142,7 @@ class TestPayrollEntry(unittest.TestCase): self.assertEqual(salary_slip.base_net_pay, payment_entry[0].total_debit) self.assertEqual(salary_slip.base_net_pay, payment_entry[0].total_credit) - def test_payroll_entry_with_employee_cost_center(self): # pylint: disable=no-self-use + def test_payroll_entry_with_employee_cost_center(self): for data in frappe.get_all("Salary Component", fields=["name"]): if not frappe.db.get_value( "Salary Component Account", {"parent": data.name, "company": "_Test Company"}, "name" @@ -356,8 +355,114 @@ class TestPayrollEntry(unittest.TestCase): if salary_slip.docstatus == 0: frappe.delete_doc("Salary Slip", name) + def test_salary_slip_operation_queueing(self): + # setup + company = erpnext.get_default_company() + company_doc = frappe.get_doc("Company", company) + employee = frappe.db.get_value("Employee", {"company": company}) + make_salary_structure( + "_Test Salary Structure", + "Monthly", + employee, + company=company, + currency=company_doc.default_currency, + ) -def make_payroll_entry(**args): + # enqueue salary slip creation via payroll entry + # Payroll Entry status should change to Queued + dates = get_start_end_dates("Monthly", nowdate()) + payroll_entry = get_payroll_entry_data( + start_date=dates.start_date, + end_date=dates.end_date, + payable_account=company_doc.default_payroll_payable_account, + currency=company_doc.default_currency, + ) + frappe.flags.enqueue_payroll_entry = True + payroll_entry.create_salary_slips() + payroll_entry.reload() + + self.assertEqual(payroll_entry.status, "Queued") + frappe.flags.enqueue_payroll_entry = False + + def test_salary_slip_operation_failure(self): + # setup + company = erpnext.get_default_company() + company_doc = frappe.get_doc("Company", company) + employee = frappe.db.get_value("Employee", {"company": company}) + salary_structure = make_salary_structure( + "_Test Salary Structure", + "Monthly", + employee, + company=company, + currency=company_doc.default_currency, + ) + + # reset account in component to test submission failure + component = frappe.get_doc("Salary Component", salary_structure.earnings[0].salary_component) + component.accounts = [] + component.save() + + # salary slip submission via payroll entry + # Payroll Entry status should change to Failed because of the missing account setup + dates = get_start_end_dates("Monthly", nowdate()) + payroll_entry = get_payroll_entry_data( + start_date=dates.start_date, + end_date=dates.end_date, + payable_account=company_doc.default_payroll_payable_account, + currency=company_doc.default_currency, + ) + payroll_entry.create_salary_slips() + payroll_entry.submit_salary_slips() + + payroll_entry.reload() + self.assertEqual(payroll_entry.status, "Failed") + self.assertIsNotNone(payroll_entry.error_message) + + # set accounts + for data in frappe.get_all("Salary Component", fields=["name"]): + if not frappe.db.get_value( + "Salary Component Account", {"parent": data.name, "company": company}, "name" + ): + set_salary_component_account(data.name, company_list=[company]) + + # Payroll Entry successful, status should change to Submitted + payroll_entry.submit_salary_slips() + payroll_entry.reload() + self.assertEqual(payroll_entry.status, "Submitted") + self.assertEqual(payroll_entry.error_message, "") + + def test_payroll_entry_status(self): + company = erpnext.get_default_company() + for data in frappe.get_all("Salary Component", fields=["name"]): + if not frappe.db.get_value( + "Salary Component Account", {"parent": data.name, "company": company}, "name" + ): + set_salary_component_account(data.name) + + employee = frappe.db.get_value("Employee", {"company": company}) + company_doc = frappe.get_doc("Company", company) + make_salary_structure( + "_Test Salary Structure", + "Monthly", + employee, + company=company, + currency=company_doc.default_currency, + ) + dates = get_start_end_dates("Monthly", nowdate()) + payroll_entry = get_payroll_entry_data( + start_date=dates.start_date, + end_date=dates.end_date, + payable_account=company_doc.default_payroll_payable_account, + currency=company_doc.default_currency, + ) + payroll_entry.submit() + self.assertEqual(payroll_entry.status, "Submitted") + + payroll_entry.cancel() + self.assertEqual(payroll_entry.status, "Cancelled") + + +def get_payroll_entry_data(**args): args = frappe._dict(args) payroll_entry = frappe.new_doc("Payroll Entry") @@ -381,7 +486,13 @@ def make_payroll_entry(**args): payroll_entry.fill_employee_details() payroll_entry.save() - payroll_entry.create_salary_slips() + + return payroll_entry + + +def make_payroll_entry(**args): + payroll_entry = get_payroll_entry_data(**args) + payroll_entry.submit() payroll_entry.submit_salary_slips() if payroll_entry.get_sal_slip_list(ss_status=1): payroll_entry.make_payment_entry() @@ -421,12 +532,3 @@ def make_holiday(holiday_list_name): ).insert() return holiday_list_name - - -def get_salary_slip(user, period, salary_structure): - salary_slip = make_employee_salary_slip(user, period, salary_structure) - salary_slip.exchange_rate = 70 - salary_slip.calculate_net_pay() - salary_slip.db_update() - - return salary_slip diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py index e9b5ed2261..9a0e8188f8 100644 --- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py @@ -169,9 +169,6 @@ def make_salary_structure( payroll_period=None, include_flexi_benefits=False, ): - if test_tax: - frappe.db.sql("""delete from `tabSalary Structure` where name=%s""", (salary_structure)) - if frappe.db.exists("Salary Structure", salary_structure): frappe.db.delete("Salary Structure", salary_structure) From 4c74637c916e8acd3a1985264fdf17ffe1788d35 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 30 May 2022 19:05:57 +0530 Subject: [PATCH 105/192] refactor: remove naming expression for payment ledger --- .../doctype/payment_ledger_entry/payment_ledger_entry.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json index d96107678f..39e90420c7 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json +++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json @@ -1,7 +1,6 @@ { "actions": [], "allow_rename": 1, - "autoname": "format:PLE-{YY}-{MM}-{######}", "creation": "2022-05-09 19:35:03.334361", "doctype": "DocType", "editable_grid": 1, @@ -138,11 +137,10 @@ "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-05-19 18:04:44.609115", + "modified": "2022-05-30 19:04:55.532171", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Ledger Entry", - "naming_rule": "Expression", "owner": "Administrator", "permissions": [ { From a0c412a0dd23aeb8181ec49cced5da4e1c908d81 Mon Sep 17 00:00:00 2001 From: Mitchy25 <42224026+Mitchy25@users.noreply.github.com> Date: Tue, 31 May 2022 15:25:09 +1200 Subject: [PATCH 106/192] Ignore Cancelled GL Entries Profitability Analysis includes 'is_cancelled' GL Entries which means that the profit numbers are incorrect. This change will ensure that the profit figures ignore cancelled GL Entries. --- .../report/profitability_analysis/profitability_analysis.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py index 3e7aa1e368..183e279fe5 100644 --- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py +++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py @@ -211,6 +211,7 @@ def set_gl_entries_by_account( {additional_conditions} and posting_date <= %(to_date)s and {based_on} is not null + and is_cancelled = 0 order by {based_on}, posting_date""".format( additional_conditions="\n".join(additional_conditions), based_on=based_on ), From 34925a3a8c4306723f7e1ccc5af2763b6f3cf2e0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 4 May 2022 17:21:19 +0530 Subject: [PATCH 107/192] fix: HRA Exemption calculation in case of multiple salary structure assignments --- erpnext/hr/utils.py | 24 +++-- erpnext/regional/india/utils.py | 152 ++++++++++++++++++++++---------- 2 files changed, 115 insertions(+), 61 deletions(-) diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 269e4aae31..c730b19924 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -439,20 +439,18 @@ def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining return False -def get_salary_assignment(employee, date): - assignment = frappe.db.sql( - """ - select * from `tabSalary Structure Assignment` - where employee=%(employee)s - and docstatus = 1 - and %(on_date)s >= from_date order by from_date desc limit 1""", - { - "employee": employee, - "on_date": date, - }, - as_dict=1, +def get_salary_assignments(employee, payroll_period): + start_date, end_date = frappe.db.get_value( + "Payroll Period", payroll_period, ["start_date", "end_date"] ) - return assignment[0] if assignment else None + assignments = frappe.db.get_all( + "Salary Structure Assignment", + filters={"employee": employee, "docstatus": 1, "from_date": ["between", (start_date, end_date)]}, + fields=["*"], + order_by="from_date", + ) + + return assignments def get_sal_slip_total_benefit_given(employee, payroll_period, component=False): diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 6a7e113390..6aa1f1f277 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -1,14 +1,24 @@ import json +import math import re import frappe from frappe import _ from frappe.model.utils import get_fetch_values -from frappe.utils import cint, cstr, date_diff, flt, getdate, nowdate +from frappe.utils import ( + add_days, + cint, + cstr, + date_diff, + flt, + get_link_to_form, + getdate, + month_diff, +) from erpnext.controllers.accounts_controller import get_taxes_and_charges from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount -from erpnext.hr.utils import get_salary_assignment +from erpnext.hr.utils import get_salary_assignments from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.regional.india import number_state_mapping, state_numbers, states @@ -360,44 +370,55 @@ def calculate_annual_eligible_hra_exemption(doc): "Company", doc.company, ["basic_component", "hra_component"] ) if not (basic_component and hra_component): - frappe.throw(_("Please mention Basic and HRA component in Company")) - annual_exemption, monthly_exemption, hra_amount = 0, 0, 0 + frappe.throw( + _("Please set Basic and HRA component in Company {0}").format( + get_link_to_form("Company", doc.company) + ) + ) + + annual_exemption = monthly_exemption = hra_amount = basic_amount = 0 + if hra_component and basic_component: - assignment = get_salary_assignment(doc.employee, nowdate()) - if assignment: - hra_component_exists = frappe.db.exists( - "Salary Detail", - { - "parent": assignment.salary_structure, - "salary_component": hra_component, - "parentfield": "earnings", - "parenttype": "Salary Structure", - }, - ) + assignments = get_salary_assignments(doc.employee, doc.payroll_period) - if hra_component_exists: - basic_amount, hra_amount = get_component_amt_from_salary_slip( - doc.employee, assignment.salary_structure, basic_component, hra_component - ) - if hra_amount: - if doc.monthly_house_rent: - annual_exemption = calculate_hra_exemption( - assignment.salary_structure, - basic_amount, - hra_amount, - doc.monthly_house_rent, - doc.rented_in_metro_city, - ) - if annual_exemption > 0: - monthly_exemption = annual_exemption / 12 - else: - annual_exemption = 0 - - elif doc.docstatus == 1: + if not assignments and doc.docstatus == 1: frappe.throw( - _("Salary Structure must be submitted before submission of Tax Ememption Declaration") + _("Salary Structure must be submitted before submission of {0}").format(doc.doctype) ) + assignment_dates = [assignment.from_date for assignment in assignments] + + for idx, assignment in enumerate(assignments): + if has_hra_component(assignment.salary_structure, hra_component): + basic_salary_amt, hra_salary_amt = get_component_amt_from_salary_slip( + doc.employee, + assignment.salary_structure, + basic_component, + hra_component, + assignment.from_date, + ) + to_date = get_end_date_for_assignment(assignment_dates, idx, doc.payroll_period) + + frequency = frappe.get_value( + "Salary Structure", assignment.salary_structure, "payroll_frequency" + ) + basic_amount += get_component_pay(frequency, basic_salary_amt, assignment.from_date, to_date) + hra_amount += get_component_pay(frequency, hra_salary_amt, assignment.from_date, to_date) + + if hra_amount: + if doc.monthly_house_rent: + annual_exemption = calculate_hra_exemption( + assignment.salary_structure, + basic_amount, + hra_amount, + doc.monthly_house_rent, + doc.rented_in_metro_city, + ) + if annual_exemption > 0: + monthly_exemption = annual_exemption / 12 + else: + annual_exemption = 0 + return frappe._dict( { "hra_amount": hra_amount, @@ -407,10 +428,44 @@ def calculate_annual_eligible_hra_exemption(doc): ) -def get_component_amt_from_salary_slip(employee, salary_structure, basic_component, hra_component): +def has_hra_component(salary_structure, hra_component): + return frappe.db.exists( + "Salary Detail", + { + "parent": salary_structure, + "salary_component": hra_component, + "parentfield": "earnings", + "parenttype": "Salary Structure", + }, + ) + + +def get_end_date_for_assignment(assignment_dates, idx, payroll_period): + end_date = None + + try: + end_date = assignment_dates[idx + 1] + end_date = add_days(end_date, -1) + except IndexError: + pass + + if not end_date: + end_date = frappe.db.get_value("Payroll Period", payroll_period, "end_date") + + return end_date + + +def get_component_amt_from_salary_slip( + employee, salary_structure, basic_component, hra_component, from_date +): salary_slip = make_salary_slip( salary_structure, employee=employee, for_preview=1, ignore_permissions=True ) + # generate salary slip as per assignment on "from_date" + salary_slip.posting_date = from_date + salary_slip.start_date = salary_slip.end_date = None + salary_slip.run_method("process_salary_structure", for_preview=True) + basic_amt, hra_amt = 0, 0 for earning in salary_slip.earnings: if earning.salary_component == basic_component: @@ -423,36 +478,37 @@ def get_component_amt_from_salary_slip(employee, salary_structure, basic_compone def calculate_hra_exemption( - salary_structure, basic, monthly_hra, monthly_house_rent, rented_in_metro_city + salary_structure, annual_basic, annual_hra, monthly_house_rent, rented_in_metro_city ): # TODO make this configurable exemptions = [] - frequency = frappe.get_value("Salary Structure", salary_structure, "payroll_frequency") # case 1: The actual amount allotted by the employer as the HRA. - exemptions.append(get_annual_component_pay(frequency, monthly_hra)) - - actual_annual_rent = monthly_house_rent * 12 - annual_basic = get_annual_component_pay(frequency, basic) + exemptions.append(annual_hra) # case 2: Actual rent paid less 10% of the basic salary. + actual_annual_rent = monthly_house_rent * 12 exemptions.append(flt(actual_annual_rent) - flt(annual_basic * 0.1)) + # case 3: 50% of the basic salary, if the employee is staying in a metro city (40% for a non-metro city). exemptions.append(annual_basic * 0.5 if rented_in_metro_city else annual_basic * 0.4) + # return minimum of 3 cases return min(exemptions) -def get_annual_component_pay(frequency, amount): +def get_component_pay(frequency, amount, from_date, to_date): + days = date_diff(to_date, from_date) + 1 + if frequency == "Daily": - return amount * 365 + return amount * days elif frequency == "Weekly": - return amount * 52 + return amount * math.ceil(days / 7) elif frequency == "Fortnightly": - return amount * 26 + return amount * math.ceil(days / 15) elif frequency == "Monthly": - return amount * 12 + return amount * month_diff(to_date, from_date) elif frequency == "Bimonthly": - return amount * 6 + return amount * math.ceil(days / 60) def validate_house_rent_dates(doc): From 2b65c9616ff81ac8e8a1d6352965adbbd5b7a21e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 11 May 2022 16:43:13 +0530 Subject: [PATCH 108/192] fix: component pay calculation --- .../salary_structure/salary_structure.py | 4 ++++ erpnext/regional/india/utils.py | 20 +++++++++---------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.py b/erpnext/payroll/doctype/salary_structure/salary_structure.py index fa36b7ab2d..edf17dbfb1 100644 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py @@ -253,6 +253,7 @@ def make_salary_slip( source_name, target_doc=None, employee=None, + posting_date=None, as_print=False, print_format=None, for_preview=0, @@ -269,6 +270,9 @@ def make_salary_slip( target.designation = employee_details.designation target.department = employee_details.department + if posting_date: + target.posting_date = posting_date + target.run_method("process_salary_structure", for_preview=for_preview) doc = get_mapped_doc( diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 6aa1f1f277..2fc1565361 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -422,8 +422,8 @@ def calculate_annual_eligible_hra_exemption(doc): return frappe._dict( { "hra_amount": hra_amount, - "annual_exemption": annual_exemption, - "monthly_exemption": monthly_exemption, + "annual_exemption": flt(annual_exemption, doc.precision("annual_hra_exemption")), + "monthly_exemption": flt(monthly_exemption, doc.precision("monthly_hra_exemption")), } ) @@ -459,12 +459,12 @@ def get_component_amt_from_salary_slip( employee, salary_structure, basic_component, hra_component, from_date ): salary_slip = make_salary_slip( - salary_structure, employee=employee, for_preview=1, ignore_permissions=True + salary_structure, + employee=employee, + for_preview=1, + ignore_permissions=True, + posting_date=from_date, ) - # generate salary slip as per assignment on "from_date" - salary_slip.posting_date = from_date - salary_slip.start_date = salary_slip.end_date = None - salary_slip.run_method("process_salary_structure", for_preview=True) basic_amt, hra_amt = 0, 0 for earning in salary_slip.earnings: @@ -502,13 +502,13 @@ def get_component_pay(frequency, amount, from_date, to_date): if frequency == "Daily": return amount * days elif frequency == "Weekly": - return amount * math.ceil(days / 7) + return amount * math.floor(days / 7) elif frequency == "Fortnightly": - return amount * math.ceil(days / 15) + return amount * math.floor(days / 14) elif frequency == "Monthly": return amount * month_diff(to_date, from_date) elif frequency == "Bimonthly": - return amount * math.ceil(days / 60) + return amount * (month_diff(to_date, from_date) / 2) def validate_house_rent_dates(doc): From 5e96a46c87e8eb861a81d74e45910187758f9c57 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 11 May 2022 16:45:20 +0530 Subject: [PATCH 109/192] test: HRA Exemption in Employee Tax Exemption Declaration --- ...test_employee_tax_exemption_declaration.py | 283 +++++++++++++++++- .../salary_structure/test_salary_structure.py | 9 +- 2 files changed, 288 insertions(+), 4 deletions(-) diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py index 1d90e7383f..6986bce670 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py @@ -4,13 +4,15 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_months, getdate import erpnext from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.utils import DuplicateDeclarationError -class TestEmployeeTaxExemptionDeclaration(unittest.TestCase): +class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): def setUp(self): make_employee("employee@taxexepmtion.com") make_employee("employee1@taxexepmtion.com") @@ -112,6 +114,257 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase): self.assertEqual(declaration.total_exemption_amount, 100000) + def test_india_hra_exemption(self): + setup_hra_exemption_prerequisites("Monthly") + employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name") + + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": employee, + "company": "Test Company", + "payroll_period": "_Test Payroll Period 1", + "currency": "INR", + "monthly_house_rent": 50000, + "rented_in_metro_city": 1, + "declarations": [ + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + amount=80000, + ), + dict( + exemption_sub_category="_Test1 Sub Category", + exemption_category="_Test Category", + amount=60000, + ), + ], + } + ).insert() + + # Monthly HRA received = 3000 + # should set HRA exemption as per actual annual HRA because that's the minimum + self.assertEqual(declaration.monthly_hra_exemption, 3000) + self.assertEqual(declaration.annual_hra_exemption, 36000) + # 100000 Standard Exemption + 36000 HRA exemption + self.assertEqual(declaration.total_exemption_amount, 136000) + + def test_india_hra_exemption_with_daily_payroll_frequency(self): + setup_hra_exemption_prerequisites("Daily") + employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name") + + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": employee, + "company": "Test Company", + "payroll_period": "_Test Payroll Period 1", + "currency": "INR", + "monthly_house_rent": 170000, + "rented_in_metro_city": 1, + "declarations": [ + dict( + exemption_sub_category="_Test1 Sub Category", + exemption_category="_Test Category", + amount=60000, + ), + ], + } + ).insert() + + # Daily HRA received = 3000 + # should set HRA exemption as per (rent - 10% of Basic Salary), that's the minimum + self.assertEqual(declaration.monthly_hra_exemption, 17916.67) + self.assertEqual(declaration.annual_hra_exemption, 215000) + # 50000 Standard Exemption + 215000 HRA exemption + self.assertEqual(declaration.total_exemption_amount, 265000) + + def test_india_hra_exemption_with_weekly_payroll_frequency(self): + setup_hra_exemption_prerequisites("Weekly") + employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name") + + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": employee, + "company": "Test Company", + "payroll_period": "_Test Payroll Period 1", + "currency": "INR", + "monthly_house_rent": 170000, + "rented_in_metro_city": 1, + "declarations": [ + dict( + exemption_sub_category="_Test1 Sub Category", + exemption_category="_Test Category", + amount=60000, + ), + ], + } + ).insert() + + # Weekly HRA received = 3000 + # should set HRA exemption as per actual annual HRA because that's the minimum + self.assertEqual(declaration.monthly_hra_exemption, 13000) + self.assertEqual(declaration.annual_hra_exemption, 156000) + # 50000 Standard Exemption + 156000 HRA exemption + self.assertEqual(declaration.total_exemption_amount, 206000) + + def test_india_hra_exemption_with_fortnightly_payroll_frequency(self): + setup_hra_exemption_prerequisites("Fortnightly") + employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name") + + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": employee, + "company": "Test Company", + "payroll_period": "_Test Payroll Period 1", + "currency": "INR", + "monthly_house_rent": 170000, + "rented_in_metro_city": 1, + "declarations": [ + dict( + exemption_sub_category="_Test1 Sub Category", + exemption_category="_Test Category", + amount=60000, + ), + ], + } + ).insert() + + # Fortnightly HRA received = 3000 + # should set HRA exemption as per actual annual HRA because that's the minimum + self.assertEqual(declaration.monthly_hra_exemption, 6500) + self.assertEqual(declaration.annual_hra_exemption, 78000) + # 50000 Standard Exemption + 78000 HRA exemption + self.assertEqual(declaration.total_exemption_amount, 128000) + + def test_india_hra_exemption_with_bimonthly_payroll_frequency(self): + setup_hra_exemption_prerequisites("Bimonthly") + employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name") + + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": employee, + "company": "Test Company", + "payroll_period": "_Test Payroll Period 1", + "currency": "INR", + "monthly_house_rent": 50000, + "rented_in_metro_city": 1, + "declarations": [ + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + amount=80000, + ), + dict( + exemption_sub_category="_Test1 Sub Category", + exemption_category="_Test Category", + amount=60000, + ), + ], + } + ).insert() + + # Bimonthly HRA received = 3000 + # should set HRA exemption as per actual annual HRA because that's the minimum + self.assertEqual(declaration.monthly_hra_exemption, 1500) + self.assertEqual(declaration.annual_hra_exemption, 18000) + # 100000 Standard Exemption + 18000 HRA exemption + self.assertEqual(declaration.total_exemption_amount, 118000) + + def test_india_hra_exemption_with_multiple_salary_structure_assignments(self): + from erpnext.payroll.doctype.salary_slip.test_salary_slip import create_tax_slab + from erpnext.payroll.doctype.salary_structure.test_salary_structure import ( + create_salary_structure_assignment, + make_salary_structure, + ) + + payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") + + create_tax_slab( + payroll_period, + allow_tax_exemption=True, + currency="INR", + effective_date=getdate("2019-04-01"), + company="_Test Company", + ) + + frappe.db.set_value( + "Company", "_Test Company", {"basic_component": "Basic Salary", "hra_component": "HRA"} + ) + + employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name") + + # salary structure with base 50000, HRA 3000 + make_salary_structure( + "Monthly Structure for HRA Exemption 1", + "Monthly", + employee=employee, + company="_Test Company", + currency="INR", + payroll_period=payroll_period.name, + from_date=payroll_period.start_date, + ) + + # salary structure with base 70000, HRA = base * 0.2 = 14000 + salary_structure = make_salary_structure( + "Monthly Structure for HRA Exemption 2", + "Monthly", + employee=employee, + company="_Test Company", + currency="INR", + payroll_period=payroll_period.name, + from_date=payroll_period.start_date, + dont_submit=True, + ) + for component_row in salary_structure.earnings: + if component_row.salary_component == "HRA": + component_row.amount = 0 + component_row.amount_based_on_formula = 1 + component_row.formula = "base * 0.2" + break + + salary_structure.submit() + + create_salary_structure_assignment( + employee, + salary_structure.name, + from_date=add_months(payroll_period.start_date, 6), + company="_Test Company", + currency="INR", + payroll_period=payroll_period.name, + base=70000, + allow_duplicate=True, + ) + + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": employee, + "company": "Test Company", + "payroll_period": "_Test Payroll Period 1", + "currency": "INR", + "monthly_house_rent": 50000, + "rented_in_metro_city": 1, + "declarations": [ + dict( + exemption_sub_category="_Test1 Sub Category", + exemption_category="_Test Category", + amount=60000, + ), + ], + } + ).insert() + + # Monthly HRA received = 50000 * 6 months + 70000 * 6 months + # should set HRA exemption as per actual annual HRA because that's the minimum + self.assertEqual(declaration.monthly_hra_exemption, 8500) + self.assertEqual(declaration.annual_hra_exemption, 102000) + # 50000 Standard Exemption + 102000 HRA exemption + self.assertEqual(declaration.total_exemption_amount, 152000) + def create_payroll_period(**args): args = frappe._dict(args) @@ -163,3 +416,31 @@ def create_exemption_category(): "is_active": 1, } ).insert() + + +def setup_hra_exemption_prerequisites(frequency): + from erpnext.payroll.doctype.salary_slip.test_salary_slip import create_tax_slab + from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure + + payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") + + create_tax_slab( + payroll_period, + allow_tax_exemption=True, + currency="INR", + effective_date=getdate("2019-04-01"), + company="_Test Company", + ) + + make_salary_structure( + f"{frequency} Structure for HRA Exemption", + frequency, + employee=frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"), + company="_Test Company", + currency="INR", + payroll_period=payroll_period, + ) + + frappe.db.set_value( + "Company", "_Test Company", {"basic_component": "Basic Salary", "hra_component": "HRA"} + ) diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py index e9b5ed2261..5c78e8f037 100644 --- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py @@ -230,9 +230,12 @@ def create_salary_structure_assignment( company=None, currency=erpnext.get_default_currency(), payroll_period=None, + base=None, + allow_duplicate=False, ): - - if frappe.db.exists("Salary Structure Assignment", {"employee": employee}): + if not allow_duplicate and frappe.db.exists( + "Salary Structure Assignment", {"employee": employee} + ): frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""", (employee)) if not payroll_period: @@ -245,7 +248,7 @@ def create_salary_structure_assignment( salary_structure_assignment = frappe.new_doc("Salary Structure Assignment") salary_structure_assignment.employee = employee - salary_structure_assignment.base = 50000 + salary_structure_assignment.base = base or 50000 salary_structure_assignment.variable = 5000 if not from_date: From 00adda7c8dab167e6679a65595f5bcb8a0f1f9d6 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 11 May 2022 18:26:33 +0530 Subject: [PATCH 110/192] fix: Tax Declaration tests and amount precision --- .../employee_tax_exemption_declaration.py | 19 ++++++-- ...test_employee_tax_exemption_declaration.py | 48 +++++++++---------- erpnext/regional/india/utils.py | 4 +- 3 files changed, 41 insertions(+), 30 deletions(-) diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py index c0ef2eee78..3d1d96598f 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py @@ -33,7 +33,9 @@ class EmployeeTaxExemptionDeclaration(Document): self.total_declared_amount += flt(d.amount) def set_total_exemption_amount(self): - self.total_exemption_amount = get_total_exemption_amount(self.declarations) + self.total_exemption_amount = flt( + get_total_exemption_amount(self.declarations), self.precision("total_exemption_amount") + ) def calculate_hra_exemption(self): self.salary_structure_hra, self.annual_hra_exemption, self.monthly_hra_exemption = 0, 0, 0 @@ -41,9 +43,18 @@ class EmployeeTaxExemptionDeclaration(Document): hra_exemption = calculate_annual_eligible_hra_exemption(self) if hra_exemption: self.total_exemption_amount += hra_exemption["annual_exemption"] - self.salary_structure_hra = hra_exemption["hra_amount"] - self.annual_hra_exemption = hra_exemption["annual_exemption"] - self.monthly_hra_exemption = hra_exemption["monthly_exemption"] + self.total_exemption_amount = flt( + self.total_exemption_amount, self.precision("total_exemption_amount") + ) + self.salary_structure_hra = flt( + hra_exemption["hra_amount"], self.precision("salary_structure_hra") + ) + self.annual_hra_exemption = flt( + hra_exemption["annual_exemption"], self.precision("annual_hra_exemption") + ) + self.monthly_hra_exemption = flt( + hra_exemption["monthly_exemption"], self.precision("monthly_hra_exemption") + ) @frappe.whitelist() diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py index 6986bce670..e158cc31bb 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py @@ -14,17 +14,18 @@ from erpnext.hr.utils import DuplicateDeclarationError class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): def setUp(self): - make_employee("employee@taxexepmtion.com") - make_employee("employee1@taxexepmtion.com") - create_payroll_period() + make_employee("employee@taxexemption.com", company="_Test Company") + make_employee("employee1@taxexemption.com", company="_Test Company") + create_payroll_period(company="_Test Company") create_exemption_category() - frappe.db.sql("""delete from `tabEmployee Tax Exemption Declaration`""") + frappe.db.delete("Employee Tax Exemption Declaration") + frappe.db.delete("Salary Structure Assignment") def test_duplicate_category_in_declaration(self): declaration = frappe.get_doc( { "doctype": "Employee Tax Exemption Declaration", - "employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"), + "employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"), "company": erpnext.get_default_company(), "payroll_period": "_Test Payroll Period", "currency": erpnext.get_default_currency(), @@ -48,7 +49,7 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): declaration = frappe.get_doc( { "doctype": "Employee Tax Exemption Declaration", - "employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"), + "employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"), "company": erpnext.get_default_company(), "payroll_period": "_Test Payroll Period", "currency": erpnext.get_default_currency(), @@ -70,7 +71,7 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): duplicate_declaration = frappe.get_doc( { "doctype": "Employee Tax Exemption Declaration", - "employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"), + "employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"), "company": erpnext.get_default_company(), "payroll_period": "_Test Payroll Period", "currency": erpnext.get_default_currency(), @@ -85,7 +86,7 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): ) self.assertRaises(DuplicateDeclarationError, duplicate_declaration.insert) duplicate_declaration.employee = frappe.get_value( - "Employee", {"user_id": "employee1@taxexepmtion.com"}, "name" + "Employee", {"user_id": "employee1@taxexemption.com"}, "name" ) self.assertTrue(duplicate_declaration.insert) @@ -93,7 +94,7 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): declaration = frappe.get_doc( { "doctype": "Employee Tax Exemption Declaration", - "employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"), + "employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"), "company": erpnext.get_default_company(), "payroll_period": "_Test Payroll Period", "currency": erpnext.get_default_currency(), @@ -116,13 +117,13 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): def test_india_hra_exemption(self): setup_hra_exemption_prerequisites("Monthly") - employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name") + employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") declaration = frappe.get_doc( { "doctype": "Employee Tax Exemption Declaration", "employee": employee, - "company": "Test Company", + "company": "_Test Company", "payroll_period": "_Test Payroll Period 1", "currency": "INR", "monthly_house_rent": 50000, @@ -151,13 +152,13 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): def test_india_hra_exemption_with_daily_payroll_frequency(self): setup_hra_exemption_prerequisites("Daily") - employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name") + employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") declaration = frappe.get_doc( { "doctype": "Employee Tax Exemption Declaration", "employee": employee, - "company": "Test Company", + "company": "_Test Company", "payroll_period": "_Test Payroll Period 1", "currency": "INR", "monthly_house_rent": 170000, @@ -181,13 +182,13 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): def test_india_hra_exemption_with_weekly_payroll_frequency(self): setup_hra_exemption_prerequisites("Weekly") - employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name") + employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") declaration = frappe.get_doc( { "doctype": "Employee Tax Exemption Declaration", "employee": employee, - "company": "Test Company", + "company": "_Test Company", "payroll_period": "_Test Payroll Period 1", "currency": "INR", "monthly_house_rent": 170000, @@ -211,13 +212,13 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): def test_india_hra_exemption_with_fortnightly_payroll_frequency(self): setup_hra_exemption_prerequisites("Fortnightly") - employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name") + employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") declaration = frappe.get_doc( { "doctype": "Employee Tax Exemption Declaration", "employee": employee, - "company": "Test Company", + "company": "_Test Company", "payroll_period": "_Test Payroll Period 1", "currency": "INR", "monthly_house_rent": 170000, @@ -241,13 +242,13 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): def test_india_hra_exemption_with_bimonthly_payroll_frequency(self): setup_hra_exemption_prerequisites("Bimonthly") - employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name") + employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") declaration = frappe.get_doc( { "doctype": "Employee Tax Exemption Declaration", "employee": employee, - "company": "Test Company", + "company": "_Test Company", "payroll_period": "_Test Payroll Period 1", "currency": "INR", "monthly_house_rent": 50000, @@ -281,6 +282,7 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): make_salary_structure, ) + employee = make_employee("employee@taxexemption2.com", company="_Test Company") payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") create_tax_slab( @@ -295,8 +297,6 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): "Company", "_Test Company", {"basic_component": "Basic Salary", "hra_component": "HRA"} ) - employee = frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name") - # salary structure with base 50000, HRA 3000 make_salary_structure( "Monthly Structure for HRA Exemption 1", @@ -343,8 +343,8 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): { "doctype": "Employee Tax Exemption Declaration", "employee": employee, - "company": "Test Company", - "payroll_period": "_Test Payroll Period 1", + "company": "_Test Company", + "payroll_period": payroll_period.name, "currency": "INR", "monthly_house_rent": 50000, "rented_in_metro_city": 1, @@ -435,7 +435,7 @@ def setup_hra_exemption_prerequisites(frequency): make_salary_structure( f"{frequency} Structure for HRA Exemption", frequency, - employee=frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"), + employee=frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"), company="_Test Company", currency="INR", payroll_period=payroll_period, diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 2fc1565361..5bbd6863d3 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -422,8 +422,8 @@ def calculate_annual_eligible_hra_exemption(doc): return frappe._dict( { "hra_amount": hra_amount, - "annual_exemption": flt(annual_exemption, doc.precision("annual_hra_exemption")), - "monthly_exemption": flt(monthly_exemption, doc.precision("monthly_hra_exemption")), + "annual_exemption": annual_exemption, + "monthly_exemption": monthly_exemption, } ) From 2e98e9e0b92c1be883419b314c0ba52888a2054c Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 23 May 2022 14:15:36 +0530 Subject: [PATCH 111/192] test: set country to India before running regional tests --- ...test_employee_tax_exemption_declaration.py | 56 ++++++++++++++++--- erpnext/regional/india/utils.py | 1 + 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py index e158cc31bb..6741854458 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py @@ -116,6 +116,10 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): self.assertEqual(declaration.total_exemption_amount, 100000) def test_india_hra_exemption(self): + # set country + current_country = frappe.flags.country + frappe.flags.country = "India" + setup_hra_exemption_prerequisites("Monthly") employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") @@ -124,7 +128,7 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): "doctype": "Employee Tax Exemption Declaration", "employee": employee, "company": "_Test Company", - "payroll_period": "_Test Payroll Period 1", + "payroll_period": "_Test Payroll Period", "currency": "INR", "monthly_house_rent": 50000, "rented_in_metro_city": 1, @@ -150,7 +154,14 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): # 100000 Standard Exemption + 36000 HRA exemption self.assertEqual(declaration.total_exemption_amount, 136000) + # reset + frappe.flags.country = current_country + def test_india_hra_exemption_with_daily_payroll_frequency(self): + # set country + current_country = frappe.flags.country + frappe.flags.country = "India" + setup_hra_exemption_prerequisites("Daily") employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") @@ -159,7 +170,7 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): "doctype": "Employee Tax Exemption Declaration", "employee": employee, "company": "_Test Company", - "payroll_period": "_Test Payroll Period 1", + "payroll_period": "_Test Payroll Period", "currency": "INR", "monthly_house_rent": 170000, "rented_in_metro_city": 1, @@ -180,7 +191,14 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): # 50000 Standard Exemption + 215000 HRA exemption self.assertEqual(declaration.total_exemption_amount, 265000) + # reset + frappe.flags.country = current_country + def test_india_hra_exemption_with_weekly_payroll_frequency(self): + # set country + current_country = frappe.flags.country + frappe.flags.country = "India" + setup_hra_exemption_prerequisites("Weekly") employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") @@ -189,7 +207,7 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): "doctype": "Employee Tax Exemption Declaration", "employee": employee, "company": "_Test Company", - "payroll_period": "_Test Payroll Period 1", + "payroll_period": "_Test Payroll Period", "currency": "INR", "monthly_house_rent": 170000, "rented_in_metro_city": 1, @@ -210,7 +228,14 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): # 50000 Standard Exemption + 156000 HRA exemption self.assertEqual(declaration.total_exemption_amount, 206000) + # reset + frappe.flags.country = current_country + def test_india_hra_exemption_with_fortnightly_payroll_frequency(self): + # set country + current_country = frappe.flags.country + frappe.flags.country = "India" + setup_hra_exemption_prerequisites("Fortnightly") employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") @@ -219,7 +244,7 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): "doctype": "Employee Tax Exemption Declaration", "employee": employee, "company": "_Test Company", - "payroll_period": "_Test Payroll Period 1", + "payroll_period": "_Test Payroll Period", "currency": "INR", "monthly_house_rent": 170000, "rented_in_metro_city": 1, @@ -240,7 +265,14 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): # 50000 Standard Exemption + 78000 HRA exemption self.assertEqual(declaration.total_exemption_amount, 128000) + # reset + frappe.flags.country = current_country + def test_india_hra_exemption_with_bimonthly_payroll_frequency(self): + # set country + current_country = frappe.flags.country + frappe.flags.country = "India" + setup_hra_exemption_prerequisites("Bimonthly") employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") @@ -249,7 +281,7 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): "doctype": "Employee Tax Exemption Declaration", "employee": employee, "company": "_Test Company", - "payroll_period": "_Test Payroll Period 1", + "payroll_period": "_Test Payroll Period", "currency": "INR", "monthly_house_rent": 50000, "rented_in_metro_city": 1, @@ -275,6 +307,9 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): # 100000 Standard Exemption + 18000 HRA exemption self.assertEqual(declaration.total_exemption_amount, 118000) + # reset + frappe.flags.country = current_country + def test_india_hra_exemption_with_multiple_salary_structure_assignments(self): from erpnext.payroll.doctype.salary_slip.test_salary_slip import create_tax_slab from erpnext.payroll.doctype.salary_structure.test_salary_structure import ( @@ -282,8 +317,12 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): make_salary_structure, ) + # set country + current_country = frappe.flags.country + frappe.flags.country = "India" + employee = make_employee("employee@taxexemption2.com", company="_Test Company") - payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") + payroll_period = create_payroll_period(name="_Test Payroll Period", company="_Test Company") create_tax_slab( payroll_period, @@ -365,6 +404,9 @@ class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): # 50000 Standard Exemption + 102000 HRA exemption self.assertEqual(declaration.total_exemption_amount, 152000) + # reset + frappe.flags.country = current_country + def create_payroll_period(**args): args = frappe._dict(args) @@ -422,7 +464,7 @@ def setup_hra_exemption_prerequisites(frequency): from erpnext.payroll.doctype.salary_slip.test_salary_slip import create_tax_slab from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure - payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") + payroll_period = create_payroll_period(name="_Test Payroll Period", company="_Test Company") create_tax_slab( payroll_period, diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 5bbd6863d3..ee48ccb24a 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -369,6 +369,7 @@ def calculate_annual_eligible_hra_exemption(doc): basic_component, hra_component = frappe.db.get_value( "Company", doc.company, ["basic_component", "hra_component"] ) + if not (basic_component and hra_component): frappe.throw( _("Please set Basic and HRA component in Company {0}").format( From cfe2f8cac14c3f37ba8df8b3c24688306b99d917 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 23 May 2022 16:17:39 +0530 Subject: [PATCH 112/192] fix: amount precision for Tax Exemption Proof Submission --- .../employee_tax_exemption_proof_submission.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py index c52efaba59..b3b66b9e7b 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py @@ -31,7 +31,9 @@ class EmployeeTaxExemptionProofSubmission(Document): self.total_actual_amount += flt(d.amount) def set_total_exemption_amount(self): - self.exemption_amount = get_total_exemption_amount(self.tax_exemption_proofs) + self.exemption_amount = flt( + get_total_exemption_amount(self.tax_exemption_proofs), self.precision("exemption_amount") + ) def calculate_hra_exemption(self): self.monthly_hra_exemption, self.monthly_house_rent, self.total_eligible_hra_exemption = 0, 0, 0 @@ -39,6 +41,13 @@ class EmployeeTaxExemptionProofSubmission(Document): hra_exemption = calculate_hra_exemption_for_period(self) if hra_exemption: self.exemption_amount += hra_exemption["total_eligible_hra_exemption"] - self.monthly_hra_exemption = hra_exemption["monthly_exemption"] - self.monthly_house_rent = hra_exemption["monthly_house_rent"] - self.total_eligible_hra_exemption = hra_exemption["total_eligible_hra_exemption"] + self.exemption_amount = flt(self.exemption_amount, self.precision("exemption_amount")) + self.monthly_hra_exemption = flt( + hra_exemption["monthly_exemption"], self.precision("monthly_hra_exemption") + ) + self.monthly_house_rent = flt( + hra_exemption["monthly_house_rent"], self.precision("monthly_house_rent") + ) + self.total_eligible_hra_exemption = flt( + hra_exemption["total_eligible_hra_exemption"], self.precision("total_eligible_hra_exemption") + ) From ed1ba677d622bb078a9e6c71611bd7ccbcbd6c0c Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 23 May 2022 16:18:11 +0530 Subject: [PATCH 113/192] test: HRA Exemption in Proof Submission --- ...test_employee_tax_exemption_declaration.py | 6 +- ...employee_tax_exemption_proof_submission.py | 83 ++++++++++++++++--- 2 files changed, 75 insertions(+), 14 deletions(-) diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py index 6741854458..2d8df35011 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py @@ -460,11 +460,13 @@ def create_exemption_category(): ).insert() -def setup_hra_exemption_prerequisites(frequency): +def setup_hra_exemption_prerequisites(frequency, employee=None): from erpnext.payroll.doctype.salary_slip.test_salary_slip import create_tax_slab from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure payroll_period = create_payroll_period(name="_Test Payroll Period", company="_Test Company") + if not employee: + employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") create_tax_slab( payroll_period, @@ -477,7 +479,7 @@ def setup_hra_exemption_prerequisites(frequency): make_salary_structure( f"{frequency} Structure for HRA Exemption", frequency, - employee=frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"), + employee=employee, company="_Test Company", currency="INR", payroll_period=payroll_period, diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py index 58b2c1af05..416cf316c9 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py @@ -4,22 +4,26 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase +from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import ( create_exemption_category, create_payroll_period, + setup_hra_exemption_prerequisites, ) -class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase): - def setup(self): - make_employee("employee@proofsubmission.com") - create_payroll_period() +class TestEmployeeTaxExemptionProofSubmission(FrappeTestCase): + def setUp(self): + make_employee("employee@proofsubmission.com", company="_Test Company") + create_payroll_period(company="_Test Company") create_exemption_category() - frappe.db.sql("""delete from `tabEmployee Tax Exemption Proof Submission`""") + frappe.db.delete("Employee Tax Exemption Proof Submission") + frappe.db.delete("Salary Structure Assignment") def test_exemption_amount_lesser_than_category_max(self): - declaration = frappe.get_doc( + proof = frappe.get_doc( { "doctype": "Employee Tax Exemption Proof Submission", "employee": frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name"), @@ -34,8 +38,8 @@ class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase): ], } ) - self.assertRaises(frappe.ValidationError, declaration.save) - declaration = frappe.get_doc( + self.assertRaises(frappe.ValidationError, proof.save) + proof = frappe.get_doc( { "doctype": "Employee Tax Exemption Proof Submission", "payroll_period": "Test Payroll Period", @@ -50,11 +54,11 @@ class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase): ], } ) - self.assertTrue(declaration.save) - self.assertTrue(declaration.submit) + self.assertTrue(proof.save) + self.assertTrue(proof.submit) def test_duplicate_category_in_proof_submission(self): - declaration = frappe.get_doc( + proof = frappe.get_doc( { "doctype": "Employee Tax Exemption Proof Submission", "employee": frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name"), @@ -74,4 +78,59 @@ class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase): ], } ) - self.assertRaises(frappe.ValidationError, declaration.save) + self.assertRaises(frappe.ValidationError, proof.save) + + def test_india_hra_exemption(self): + # set country + current_country = frappe.flags.country + frappe.flags.country = "India" + + employee = frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name") + setup_hra_exemption_prerequisites("Monthly", employee) + payroll_period = frappe.db.get_value( + "Payroll Period", "_Test Payroll Period", ["start_date", "end_date"], as_dict=True + ) + + proof = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Proof Submission", + "employee": employee, + "company": "_Test Company", + "payroll_period": "_Test Payroll Period", + "currency": "INR", + "house_rent_payment_amount": 600000, + "rented_in_metro_city": 1, + "rented_from_date": payroll_period.start_date, + "rented_to_date": payroll_period.end_date, + "tax_exemption_proofs": [ + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + type_of_proof="Test Proof", + amount=100000, + ), + dict( + exemption_sub_category="_Test1 Sub Category", + exemption_category="_Test Category", + type_of_proof="Test Proof", + amount=50000, + ), + ], + } + ).insert() + + self.assertEqual(proof.monthly_house_rent, 50000) + + # Monthly HRA received = 3000 + # should set HRA exemption as per actual annual HRA because that's the minimum + self.assertEqual(proof.monthly_hra_exemption, 3000) + self.assertEqual(proof.total_eligible_hra_exemption, 36000) + + # total exemptions + house rent payment amount + self.assertEqual(proof.total_actual_amount, 750000) + + # 100000 Standard Exemption + 36000 HRA exemption + self.assertEqual(proof.exemption_amount, 136000) + + # reset + frappe.flags.country = current_country From a8f98f3f9684afcf5675876f95d9896461291563 Mon Sep 17 00:00:00 2001 From: maharshivpatel <39730881+maharshivpatel@users.noreply.github.com> Date: Tue, 31 May 2022 12:14:39 +0530 Subject: [PATCH 114/192] feat(india): Improve E-way Bill Cancellation. (#31088) --- erpnext/regional/india/e_invoice/einvoice.js | 41 +++---- erpnext/regional/india/e_invoice/utils.py | 109 +++++++++++++++---- 2 files changed, 110 insertions(+), 40 deletions(-) diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index ef24ce791c..580e6469e2 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -150,26 +150,29 @@ erpnext.setup_einvoice_actions = (doctype) => { if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) { const action = () => { - let message = __('Cancellation of e-way bill is currently not supported.') + ' '; - message += '

'; - message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.'); - - const dialog = frappe.msgprint({ - title: __('Update E-Way Bill Cancelled Status?'), - message: message, - indicator: 'orange', - primary_action: { - action: function() { - frappe.call({ - method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill', - args: { doctype, docname: name }, - freeze: true, - callback: () => frm.reload_doc() && dialog.hide() - }); - } + // This confirm is added to just reduce unnecesory API calls. All required logic is implemented on server side. + frappe.confirm( + __("Have you cancelled e-way bill on the portal?"), + () => { + frappe.call({ + method: "erpnext.regional.india.e_invoice.utils.cancel_eway_bill", + args: { doctype, docname: name }, + freeze: true, + callback: () => frm.reload_doc(), + }); }, - primary_action_label: __('Yes') - }); + () => { + frappe.show_alert( + { + message: __( + "Please cancel e-way bill on the portal first." + ), + indicator: "orange", + }, + 5 + ); + } + ); }; add_custom_button(__("Cancel E-Way Bill"), action); } diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index e5a1a59e42..9add09beaf 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -803,6 +803,8 @@ class GSPConnector: self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin" # cancel_ewaybill_url will only work if user have bought ewb api from adaequare. self.cancel_ewaybill_url = self.base_url + "/enriched/ewb/ewayapi?action=CANEWB" + # ewaybill_details_url + ?irn={irn_number} will provide eway bill number and details. + self.ewaybill_details_url = self.base_url + "/enriched/ei/api/ewaybill/irn" self.generate_ewaybill_url = self.base_url + "/enriched/ei/api/ewaybill" self.get_qrcode_url = self.base_url + "/enriched/ei/others/qr/image" @@ -1205,23 +1207,22 @@ class GSPConnector: log_error(data) self.raise_error(True) - def cancel_eway_bill(self, eway_bill, reason, remark): + def get_ewb_details(self): + """ + Get e-Waybill Details by IRN API documentaion for validation is not added yet. + https://einv-apisandbox.nic.in/version1.03/get-ewaybill-details-by-irn.html#validations + NOTE: if ewaybill Validity period lapsed or scanned by officer enroute (not tested yet) it will still return status as "ACT". + """ headers = self.get_headers() - data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4) - headers["username"] = headers["user_name"] - del headers["user_name"] - try: - res = self.make_request("post", self.cancel_ewaybill_url, headers, data) - if res.get("success"): - self.invoice.ewaybill = "" - self.invoice.eway_bill_cancelled = 1 - self.invoice.flags.updater_reference = { - "doctype": self.invoice.doctype, - "docname": self.invoice.name, - "label": _("E-Way Bill Cancelled - {}").format(remark), - } - self.update_invoice() + irn = self.invoice.irn + if not irn: + frappe.throw(_("IRN is mandatory to get E-Waybill Details. Please generate IRN first.")) + try: + params = "?irn={irn}".format(irn=irn) + res = self.make_request("get", self.ewaybill_details_url + params, headers) + if res.get("success"): + return res.get("result") else: raise RequestFailed @@ -1230,9 +1231,65 @@ class GSPConnector: self.raise_error(errors=errors) except Exception: - log_error(data) + log_error() self.raise_error(True) + def update_ewb_details(self, ewb_details=None): + # for any reason user chooses to generate eway bill using portal this will allow to update ewaybill details in the invoice. + if not self.invoice.irn: + frappe.throw(_("IRN is mandatory to update E-Waybill Details. Please generate IRN first.")) + if not ewb_details: + ewb_details = self.get_ewb_details() + if ewb_details: + self.invoice.ewaybill = ewb_details.get("EwbNo") + self.invoice.eway_bill_validity = ewb_details.get("EwbValidTill") + self.invoice.eway_bill_cancelled = 0 if ewb_details.get("Status") == "ACT" else 1 + self.update_invoice() + + def cancel_eway_bill(self): + ewb_details = self.get_ewb_details() + if ewb_details: + ewb_no = str(ewb_details.get("EwbNo")) + ewb_status = ewb_details.get("Status") + if ewb_status == "CNL": + self.invoice.ewaybill = "" + self.invoice.eway_bill_cancelled = 1 + self.invoice.flags.updater_reference = { + "doctype": self.invoice.doctype, + "docname": self.invoice.name, + "label": _("E-Way Bill Cancelled"), + } + self.update_invoice() + frappe.msgprint( + _("E-Way Bill Cancelled successfully"), + indicator="green", + alert=True, + ) + elif ewb_status == "ACT" and self.invoice.ewaybill == ewb_no: + msg = _("E-Way Bill {} is still active.").format(bold(ewb_no)) + msg += "

" + msg += _( + "You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system." + ) + frappe.msgprint(msg) + elif ewb_status == "ACT" and self.invoice.ewaybill != ewb_no: + # if user cancelled the current eway bill and generated new eway bill using portal, then this will update new ewb number in sales invoice. + msg = _("E-Way Bill No. {0} doesn't match {1} saved in the invoice.").format( + bold(ewb_no), bold(self.invoice.ewaybill) + ) + msg += "
" + msg += _("E-Way Bill No. {} is updated in the invoice.").format(bold(ewb_no)) + frappe.msgprint(msg) + self.update_ewb_details(ewb_details=ewb_details) + else: + # this block should not be ever called but added incase there is any change in API. + msg = _("Unknown E-Way Status Code {}.").format(ewb_status) + msg += "

" + msg += _("Please contact your system administrator.") + frappe.throw(msg) + else: + frappe.msgprint(_("E-Way Bill Details not found for this IRN.")) + def sanitize_error_message(self, message): """ On validation errors, response message looks something like this: @@ -1383,12 +1440,22 @@ def generate_eway_bill(doctype, docname, **kwargs): @frappe.whitelist() def cancel_eway_bill(doctype, docname): - # NOTE: cancel_eway_bill api is disabled by Adequare. - # gsp_connector = GSPConnector(doctype, docname) - # gsp_connector.cancel_eway_bill(eway_bill, reason, remark) + # NOTE: cancel_eway_bill api is disabled by NIC for E-invoice so this will only check if eway bill is canceled or not and update accordingly. + # https://einv-apisandbox.nic.in/version1.03/cancel-eway-bill.html# + gsp_connector = GSPConnector(doctype, docname) + gsp_connector.cancel_eway_bill() - frappe.db.set_value(doctype, docname, "ewaybill", "") - frappe.db.set_value(doctype, docname, "eway_bill_cancelled", 1) + +@frappe.whitelist() +def get_ewb_details(doctype, docname): + gsp_connector = GSPConnector(doctype, docname) + gsp_connector.get_ewb_details() + + +@frappe.whitelist() +def update_ewb_details(doctype, docname): + gsp_connector = GSPConnector(doctype, docname) + gsp_connector.update_ewb_details() @frappe.whitelist() From ddb46c571133c92d4151b65350d896857d38edf1 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 31 May 2022 14:14:27 +0530 Subject: [PATCH 115/192] fix: batch selector flag (#31191) This is broken again after serializing scan actions, which causes selector to trigger before batch_no is set. Solution: for duration of scan disable the selector --- erpnext/public/js/controllers/transaction.js | 6 ------ erpnext/public/js/utils/barcode_scanner.js | 7 ++++++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index d11205a1ad..edc4b06dca 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -526,12 +526,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe if(!d[k]) d[k] = v; }); - if (d.__disable_batch_serial_selector) { - // reset for future use. - d.__disable_batch_serial_selector = false; - return; - } - if (d.has_batch_no && d.has_serial_no) { d.batch_no = undefined; } diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js index eea91ef5fe..0356fdcd05 100644 --- a/erpnext/public/js/utils/barcode_scanner.js +++ b/erpnext/public/js/utils/barcode_scanner.js @@ -98,6 +98,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { () => this.set_batch_no(row, batch_no), () => this.set_barcode(row, barcode), () => this.clean_up(), + () => this.revert_selector_flag(row, data), () => resolve(row) ]); }); @@ -112,10 +113,14 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { const require_selecting_serial = has_serial_no && !serial_no; if (!(require_selecting_batch || require_selecting_serial)) { - row.__disable_batch_serial_selector = true; + frappe.flags.hide_serial_batch_dialog = true; } } + revert_selector_flag() { + frappe.flags.hide_serial_batch_dialog = false; + } + set_item(row, item_code) { return new Promise(resolve => { const increment = async (value = 1) => { From 691b34a8eda3d415ebffc9da3baf1d8e9ca2adb4 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 31 May 2022 14:15:13 +0530 Subject: [PATCH 116/192] chore: unnessary args --- erpnext/public/js/utils/barcode_scanner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js index 0356fdcd05..943db07705 100644 --- a/erpnext/public/js/utils/barcode_scanner.js +++ b/erpnext/public/js/utils/barcode_scanner.js @@ -98,7 +98,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { () => this.set_batch_no(row, batch_no), () => this.set_barcode(row, barcode), () => this.clean_up(), - () => this.revert_selector_flag(row, data), + () => this.revert_selector_flag(), () => resolve(row) ]); }); From a1b7a7983a92906d10c7b860f37167568ab279b6 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 31 May 2022 15:35:40 +0530 Subject: [PATCH 117/192] refactor!: drop naming series tool (#31183) --- .../buying_settings/buying_settings.py | 2 +- .../doctype/website_item/website_item.py | 4 +- erpnext/hr/doctype/hr_settings/hr_settings.py | 2 +- erpnext/patches.txt | 1 + .../v13_0/item_naming_series_not_mandatory.py | 2 +- .../selling_settings/selling_settings.py | 2 +- erpnext/setup/doctype/naming_series/README.md | 1 - .../setup/doctype/naming_series/__init__.py | 0 .../doctype/naming_series/naming_series.js | 88 ----- .../doctype/naming_series/naming_series.json | 132 -------- .../doctype/naming_series/naming_series.py | 303 ------------------ .../naming_series/test_naming_series.py | 35 -- .../doctype/stock_settings/stock_settings.py | 2 +- erpnext/utilities/naming.py | 60 ++++ 14 files changed, 67 insertions(+), 567 deletions(-) delete mode 100644 erpnext/setup/doctype/naming_series/README.md delete mode 100644 erpnext/setup/doctype/naming_series/__init__.py delete mode 100644 erpnext/setup/doctype/naming_series/naming_series.js delete mode 100644 erpnext/setup/doctype/naming_series/naming_series.json delete mode 100644 erpnext/setup/doctype/naming_series/naming_series.py delete mode 100644 erpnext/setup/doctype/naming_series/test_naming_series.py create mode 100644 erpnext/utilities/naming.py diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.py b/erpnext/buying/doctype/buying_settings/buying_settings.py index c52b59e4c0..7b18cdbedc 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.py +++ b/erpnext/buying/doctype/buying_settings/buying_settings.py @@ -18,7 +18,7 @@ class BuyingSettings(Document): for key in ["supplier_group", "supp_master_name", "maintain_same_rate", "buying_price_list"]: frappe.db.set_default(key, self.get(key, "")) - from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series + from erpnext.utilities.naming import set_by_naming_series set_by_naming_series( "Supplier", diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py index 02ec3bf1f3..f6fea72f8a 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.py +++ b/erpnext/e_commerce/doctype/website_item/website_item.py @@ -34,9 +34,7 @@ class WebsiteItem(WebsiteGenerator): def autoname(self): # use naming series to accomodate items with same name (different item code) - from frappe.model.naming import make_autoname - - from erpnext.setup.doctype.naming_series.naming_series import get_default_naming_series + from frappe.model.naming import get_default_naming_series, make_autoname naming_series = get_default_naming_series("Website Item") if not self.name and naming_series: diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.py b/erpnext/hr/doctype/hr_settings/hr_settings.py index 72a49e285a..b56f3dbe0d 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.py +++ b/erpnext/hr/doctype/hr_settings/hr_settings.py @@ -22,7 +22,7 @@ class HRSettings(Document): PROCEED_WITH_FREQUENCY_CHANGE = False def set_naming_series(self): - from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series + from erpnext.utilities.naming import set_by_naming_series set_by_naming_series( "Employee", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 8c0ebe7a90..785e2baa11 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -372,3 +372,4 @@ erpnext.patches.v14_0.discount_accounting_separation erpnext.patches.v14_0.delete_employee_transfer_property_doctype erpnext.patches.v13_0.create_accounting_dimensions_in_orders erpnext.patches.v13_0.set_per_billed_in_return_delivery_note +execute:frappe.delete_doc("DocType", "Naming Series") diff --git a/erpnext/patches/v13_0/item_naming_series_not_mandatory.py b/erpnext/patches/v13_0/item_naming_series_not_mandatory.py index 33fb8f963c..0235a621ce 100644 --- a/erpnext/patches/v13_0/item_naming_series_not_mandatory.py +++ b/erpnext/patches/v13_0/item_naming_series_not_mandatory.py @@ -1,6 +1,6 @@ import frappe -from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series +from erpnext.utilities.naming import set_by_naming_series def execute(): diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py index 6c09894251..d977807e7d 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.py +++ b/erpnext/selling/doctype/selling_settings/selling_settings.py @@ -27,7 +27,7 @@ class SellingSettings(Document): ]: frappe.db.set_default(key, self.get(key, "")) - from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series + from erpnext.utilities.naming import set_by_naming_series set_by_naming_series( "Customer", diff --git a/erpnext/setup/doctype/naming_series/README.md b/erpnext/setup/doctype/naming_series/README.md deleted file mode 100644 index 5a9b8ca861..0000000000 --- a/erpnext/setup/doctype/naming_series/README.md +++ /dev/null @@ -1 +0,0 @@ -Tool to set numbering (naming) series for various DocTypes. \ No newline at end of file diff --git a/erpnext/setup/doctype/naming_series/__init__.py b/erpnext/setup/doctype/naming_series/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/setup/doctype/naming_series/naming_series.js b/erpnext/setup/doctype/naming_series/naming_series.js deleted file mode 100644 index 0fb72abba6..0000000000 --- a/erpnext/setup/doctype/naming_series/naming_series.js +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - - -frappe.ui.form.on("Naming Series", { - onload: function(frm) { - frm.events.get_doc_and_prefix(frm); - }, - - refresh: function(frm) { - frm.disable_save(); - }, - - get_doc_and_prefix: function(frm) { - frappe.call({ - method: "get_transactions", - doc: frm.doc, - callback: function(r) { - frm.set_df_property("select_doc_for_series", "options", r.message.transactions); - frm.set_df_property("prefix", "options", r.message.prefixes); - } - }); - }, - - select_doc_for_series: function(frm) { - frm.set_value("user_must_always_select", 0); - frappe.call({ - method: "get_options", - doc: frm.doc, - callback: function(r) { - frm.set_value("set_options", r.message); - if(r.message && r.message.split('\n')[0]=='') - frm.set_value('user_must_always_select', 1); - frm.refresh(); - } - }); - }, - - prefix: function(frm) { - frappe.call({ - method: "get_current", - doc: frm.doc, - callback: function(r) { - frm.refresh_field("current_value"); - } - }); - }, - - update: function(frm) { - frappe.call({ - method: "update_series", - doc: frm.doc, - callback: function(r) { - frm.events.get_doc_and_prefix(frm); - } - }); - }, - - naming_series_to_check(frm) { - frappe.call({ - method: "preview_series", - doc: frm.doc, - callback: function(r) { - if (!r.exc) { - frm.set_value("preview", r.message); - } else { - frm.set_value("preview", __("Failed to generate preview of series")); - } - } - }); - }, - - add_series(frm) { - const series = frm.doc.naming_series_to_check; - - if (!series) { - frappe.show_alert(__("Please type a valid series.")); - return; - } - - if (!frm.doc.set_options.includes(series)) { - const current_series = frm.doc.set_options; - frm.set_value("set_options", `${current_series}\n${series}`); - } else { - frappe.show_alert(__("Series already added to transaction.")); - } - }, -}); diff --git a/erpnext/setup/doctype/naming_series/naming_series.json b/erpnext/setup/doctype/naming_series/naming_series.json deleted file mode 100644 index c65a6f0ae4..0000000000 --- a/erpnext/setup/doctype/naming_series/naming_series.json +++ /dev/null @@ -1,132 +0,0 @@ -{ - "actions": [], - "creation": "2022-05-26 03:12:49.087648", - "description": "Set prefix for numbering series on your transactions", - "doctype": "DocType", - "engine": "InnoDB", - "field_order": [ - "setup_series", - "select_doc_for_series", - "help_html", - "naming_series_to_check", - "preview", - "add_series", - "set_options", - "user_must_always_select", - "update", - "column_break_13", - "update_series", - "prefix", - "current_value", - "update_series_start" - ], - "fields": [ - { - "description": "Set prefix for numbering series on your transactions", - "fieldname": "setup_series", - "fieldtype": "Section Break", - "label": "Setup Series" - }, - { - "fieldname": "select_doc_for_series", - "fieldtype": "Select", - "label": "Select Transaction" - }, - { - "depends_on": "select_doc_for_series", - "fieldname": "help_html", - "fieldtype": "HTML", - "label": "Help HTML", - "options": "
\n Edit list of Series in the box below. Rules:\n
    \n
  • Each Series Prefix on a new line.
  • \n
  • Allowed special characters are \"/\" and \"-\"
  • \n
  • \n Optionally, set the number of digits in the series using dot (.)\n followed by hashes (#). For example, \".####\" means that the series\n will have four digits. Default is five digits.\n
  • \n
  • \n You can also use variables in the series name by putting them\n between (.) dots\n
    \n Support Variables:\n
      \n
    • .YYYY. - Year in 4 digits
    • \n
    • .YY. - Year in 2 digits
    • \n
    • .MM. - Month
    • \n
    • .DD. - Day of month
    • \n
    • .WW. - Week of the year
    • \n
    • .FY. - Fiscal Year
    • \n
    • \n .{fieldname}. - fieldname on the document e.g.\n branch\n
    • \n
    \n
  • \n
\n Examples:\n
    \n
  • INV-
  • \n
  • INV-10-
  • \n
  • INVK-
  • \n
  • INV-.YYYY.-.{branch}.-.MM.-.####
  • \n
\n
\n
\n" - }, - { - "depends_on": "select_doc_for_series", - "fieldname": "set_options", - "fieldtype": "Text", - "label": "Series List for this Transaction" - }, - { - "default": "0", - "depends_on": "select_doc_for_series", - "description": "Check this if you want to force the user to select a series before saving. There will be no default if you check this.", - "fieldname": "user_must_always_select", - "fieldtype": "Check", - "label": "User must always select" - }, - { - "depends_on": "select_doc_for_series", - "fieldname": "update", - "fieldtype": "Button", - "label": "Update" - }, - { - "description": "Change the starting / current sequence number of an existing series.", - "fieldname": "update_series", - "fieldtype": "Section Break", - "label": "Update Series" - }, - { - "fieldname": "prefix", - "fieldtype": "Select", - "label": "Prefix" - }, - { - "description": "This is the number of the last created transaction with this prefix", - "fieldname": "current_value", - "fieldtype": "Int", - "label": "Current Value" - }, - { - "fieldname": "update_series_start", - "fieldtype": "Button", - "label": "Update Series Number", - "options": "update_series_start" - }, - { - "fieldname": "naming_series_to_check", - "fieldtype": "Data", - "label": "Try a naming Series" - }, - { - "default": " ", - "fieldname": "preview", - "fieldtype": "Text", - "label": "Preview of generated names", - "read_only": 1 - }, - { - "fieldname": "column_break_13", - "fieldtype": "Column Break" - }, - { - "fieldname": "add_series", - "fieldtype": "Button", - "label": "Add this Series" - } - ], - "hide_toolbar": 1, - "icon": "fa fa-sort-by-order", - "idx": 1, - "issingle": 1, - "links": [], - "modified": "2022-05-26 06:06:42.109504", - "modified_by": "Administrator", - "module": "Setup", - "name": "Naming Series", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "read_only": 1, - "sort_field": "modified", - "sort_order": "DESC", - "states": [] -} \ No newline at end of file diff --git a/erpnext/setup/doctype/naming_series/naming_series.py b/erpnext/setup/doctype/naming_series/naming_series.py deleted file mode 100644 index eafc264f30..0000000000 --- a/erpnext/setup/doctype/naming_series/naming_series.py +++ /dev/null @@ -1,303 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - - -import frappe -from frappe import _, msgprint, throw -from frappe.core.doctype.doctype.doctype import validate_series -from frappe.model.document import Document -from frappe.model.naming import make_autoname, parse_naming_series -from frappe.permissions import get_doctypes_with_read -from frappe.utils import cint, cstr - - -class NamingSeriesNotSetError(frappe.ValidationError): - pass - - -class NamingSeries(Document): - @frappe.whitelist() - def get_transactions(self, arg=None): - doctypes = list( - set( - frappe.db.sql_list( - """select parent - from `tabDocField` df where fieldname='naming_series'""" - ) - + frappe.db.sql_list( - """select dt from `tabCustom Field` - where fieldname='naming_series'""" - ) - ) - ) - - doctypes = list(set(get_doctypes_with_read()).intersection(set(doctypes))) - prefixes = "" - for d in doctypes: - options = "" - try: - options = self.get_options(d) - except frappe.DoesNotExistError: - frappe.msgprint(_("Unable to find DocType {0}").format(d)) - # frappe.pass_does_not_exist_error() - continue - - if options: - prefixes = prefixes + "\n" + options - prefixes.replace("\n\n", "\n") - prefixes = prefixes.split("\n") - - custom_prefixes = frappe.get_all( - "DocType", - fields=["autoname"], - filters={ - "name": ("not in", doctypes), - "autoname": ("like", "%.#%"), - "module": ("not in", ["Core"]), - }, - ) - if custom_prefixes: - prefixes = prefixes + [d.autoname.rsplit(".", 1)[0] for d in custom_prefixes] - - prefixes = "\n".join(sorted(prefixes)) - - return {"transactions": "\n".join([""] + sorted(doctypes)), "prefixes": prefixes} - - def scrub_options_list(self, ol): - options = list(filter(lambda x: x, [cstr(n).strip() for n in ol])) - return options - - @frappe.whitelist() - def update_series(self, arg=None): - """update series list""" - self.validate_series_set() - self.check_duplicate() - series_list = self.set_options.split("\n") - - # set in doctype - self.set_series_for(self.select_doc_for_series, series_list) - - # create series - map(self.insert_series, [d.split(".")[0] for d in series_list if d.strip()]) - - msgprint(_("Series Updated")) - - return self.get_transactions() - - def validate_series_set(self): - if self.select_doc_for_series and not self.set_options: - frappe.throw(_("Please set the series to be used.")) - - def set_series_for(self, doctype, ol): - options = self.scrub_options_list(ol) - - # validate names - for i in options: - self.validate_series_name(i) - - if options and self.user_must_always_select: - options = [""] + options - - default = options[0] if options else "" - - # update in property setter - prop_dict = {"options": "\n".join(options), "default": default} - - for prop in prop_dict: - ps_exists = frappe.db.get_value( - "Property Setter", {"field_name": "naming_series", "doc_type": doctype, "property": prop} - ) - - if ps_exists: - ps = frappe.get_doc("Property Setter", ps_exists) - ps.value = prop_dict[prop] - ps.save() - else: - ps = frappe.get_doc( - { - "doctype": "Property Setter", - "doctype_or_field": "DocField", - "doc_type": doctype, - "field_name": "naming_series", - "property": prop, - "value": prop_dict[prop], - "property_type": "Text", - "__islocal": 1, - } - ) - ps.save() - - self.set_options = "\n".join(options) - - frappe.clear_cache(doctype=doctype) - - def check_duplicate(self): - parent = list( - set( - frappe.db.sql_list( - """select dt.name - from `tabDocField` df, `tabDocType` dt - where dt.name = df.parent and df.fieldname='naming_series' and dt.name != %s""", - self.select_doc_for_series, - ) - + frappe.db.sql_list( - """select dt.name - from `tabCustom Field` df, `tabDocType` dt - where dt.name = df.dt and df.fieldname='naming_series' and dt.name != %s""", - self.select_doc_for_series, - ) - ) - ) - sr = [[frappe.get_meta(p).get_field("naming_series").options, p] for p in parent] - dt = frappe.get_doc("DocType", self.select_doc_for_series) - options = self.scrub_options_list(self.set_options.split("\n")) - for series in options: - validate_series(dt, series) - for i in sr: - if i[0]: - existing_series = [d.split(".")[0] for d in i[0].split("\n")] - if series.split(".")[0] in existing_series: - frappe.throw(_("Series {0} already used in {1}").format(series, i[1])) - - def validate_series_name(self, n): - import re - - if not re.match(r"^[\w\- \/.#{}]+$", n, re.UNICODE): - throw( - _('Special Characters except "-", "#", ".", "/", "{" and "}" not allowed in naming series') - ) - - @frappe.whitelist() - def get_options(self, arg=None): - if frappe.get_meta(arg or self.select_doc_for_series).get_field("naming_series"): - return frappe.get_meta(arg or self.select_doc_for_series).get_field("naming_series").options - - @frappe.whitelist() - def get_current(self, arg=None): - """get series current""" - if self.prefix: - prefix = self.parse_naming_series() - self.current_value = frappe.db.get_value("Series", prefix, "current", order_by="name") - - def insert_series(self, series): - """insert series if missing""" - if frappe.db.get_value("Series", series, "name", order_by="name") == None: - frappe.db.sql("insert into tabSeries (name, current) values (%s, 0)", (series)) - - @frappe.whitelist() - def update_series_start(self): - if self.prefix: - prefix = self.parse_naming_series() - self.insert_series(prefix) - frappe.db.sql( - "update `tabSeries` set current = %s where name = %s", (cint(self.current_value), prefix) - ) - msgprint(_("Series Updated Successfully")) - else: - msgprint(_("Please select prefix first")) - - def parse_naming_series(self): - parts = self.prefix.split(".") - - # Remove ### from the end of series - if parts[-1] == "#" * len(parts[-1]): - del parts[-1] - - prefix = parse_naming_series(parts) - return prefix - - @frappe.whitelist() - def preview_series(self) -> str: - """Preview what the naming series will generate.""" - - generated_names = [] - series = self.naming_series_to_check - if not series: - return "" - - try: - doc = self._fetch_last_doc_if_available() - for _count in range(3): - generated_names.append(make_autoname(series, doc=doc)) - except Exception as e: - if frappe.message_log: - frappe.message_log.pop() - return _("Failed to generate names from the series") + f"\n{str(e)}" - - # Explcitly rollback in case any changes were made to series table. - frappe.db.rollback() # nosemgrep - return "\n".join(generated_names) - - def _fetch_last_doc_if_available(self): - """Fetch last doc for evaluating naming series with fields.""" - try: - return frappe.get_last_doc(self.select_doc_for_series) - except Exception: - return None - - -def set_by_naming_series( - doctype, fieldname, naming_series, hide_name_field=True, make_mandatory=1 -): - from frappe.custom.doctype.property_setter.property_setter import make_property_setter - - if naming_series: - make_property_setter( - doctype, "naming_series", "hidden", 0, "Check", validate_fields_for_doctype=False - ) - make_property_setter( - doctype, "naming_series", "reqd", make_mandatory, "Check", validate_fields_for_doctype=False - ) - - # set values for mandatory - try: - frappe.db.sql( - """update `tab{doctype}` set naming_series={s} where - ifnull(naming_series, '')=''""".format( - doctype=doctype, s="%s" - ), - get_default_naming_series(doctype), - ) - except NamingSeriesNotSetError: - pass - - if hide_name_field: - make_property_setter(doctype, fieldname, "reqd", 0, "Check", validate_fields_for_doctype=False) - make_property_setter( - doctype, fieldname, "hidden", 1, "Check", validate_fields_for_doctype=False - ) - else: - make_property_setter( - doctype, "naming_series", "reqd", 0, "Check", validate_fields_for_doctype=False - ) - make_property_setter( - doctype, "naming_series", "hidden", 1, "Check", validate_fields_for_doctype=False - ) - - if hide_name_field: - make_property_setter( - doctype, fieldname, "hidden", 0, "Check", validate_fields_for_doctype=False - ) - make_property_setter(doctype, fieldname, "reqd", 1, "Check", validate_fields_for_doctype=False) - - # set values for mandatory - frappe.db.sql( - """update `tab{doctype}` set `{fieldname}`=`name` where - ifnull({fieldname}, '')=''""".format( - doctype=doctype, fieldname=fieldname - ) - ) - - -def get_default_naming_series(doctype): - naming_series = frappe.get_meta(doctype).get_field("naming_series").options or "" - naming_series = naming_series.split("\n") - out = naming_series[0] or (naming_series[1] if len(naming_series) > 1 else None) - - if not out: - frappe.throw( - _("Please set Naming Series for {0} via Setup > Settings > Naming Series").format(doctype), - NamingSeriesNotSetError, - ) - else: - return out diff --git a/erpnext/setup/doctype/naming_series/test_naming_series.py b/erpnext/setup/doctype/naming_series/test_naming_series.py deleted file mode 100644 index fce663e4c5..0000000000 --- a/erpnext/setup/doctype/naming_series/test_naming_series.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import frappe -from frappe.tests.utils import FrappeTestCase - -from erpnext.setup.doctype.naming_series.naming_series import NamingSeries - - -class TestNamingSeries(FrappeTestCase): - def setUp(self): - self.ns: NamingSeries = frappe.get_doc("Naming Series") - - def tearDown(self): - frappe.db.rollback() - - def test_naming_preview(self): - self.ns.select_doc_for_series = "Sales Invoice" - - self.ns.naming_series_to_check = "AXBZ.####" - serieses = self.ns.preview_series().split("\n") - self.assertEqual(["AXBZ0001", "AXBZ0002", "AXBZ0003"], serieses) - - self.ns.naming_series_to_check = "AXBZ-.{currency}.-" - serieses = self.ns.preview_series().split("\n") - - def test_get_transactions(self): - - naming_info = self.ns.get_transactions() - self.assertIn("Sales Invoice", naming_info["transactions"]) - - existing_naming_series = frappe.get_meta("Sales Invoice").get_field("naming_series").options - - for series in existing_naming_series.split("\n"): - self.assertIn(series, naming_info["prefixes"]) diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index e592a4be3c..50807a96ab 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -26,7 +26,7 @@ class StockSettings(Document): ]: frappe.db.set_default(key, self.get(key, "")) - from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series + from erpnext.utilities.naming import set_by_naming_series set_by_naming_series( "Item", diff --git a/erpnext/utilities/naming.py b/erpnext/utilities/naming.py new file mode 100644 index 0000000000..52bbadef14 --- /dev/null +++ b/erpnext/utilities/naming.py @@ -0,0 +1,60 @@ +import frappe +from frappe.model.naming import get_default_naming_series + + +class NamingSeriesNotSetError(frappe.ValidationError): + pass + + +def set_by_naming_series( + doctype, fieldname, naming_series, hide_name_field=True, make_mandatory=1 +): + """Change a doctype's naming to user naming series""" + from frappe.custom.doctype.property_setter.property_setter import make_property_setter + + if naming_series: + make_property_setter( + doctype, "naming_series", "hidden", 0, "Check", validate_fields_for_doctype=False + ) + make_property_setter( + doctype, "naming_series", "reqd", make_mandatory, "Check", validate_fields_for_doctype=False + ) + + # set values for mandatory + try: + frappe.db.sql( + """update `tab{doctype}` set naming_series={s} where + ifnull(naming_series, '')=''""".format( + doctype=doctype, s="%s" + ), + get_default_naming_series(doctype), + ) + except NamingSeriesNotSetError: + pass + + if hide_name_field: + make_property_setter(doctype, fieldname, "reqd", 0, "Check", validate_fields_for_doctype=False) + make_property_setter( + doctype, fieldname, "hidden", 1, "Check", validate_fields_for_doctype=False + ) + else: + make_property_setter( + doctype, "naming_series", "reqd", 0, "Check", validate_fields_for_doctype=False + ) + make_property_setter( + doctype, "naming_series", "hidden", 1, "Check", validate_fields_for_doctype=False + ) + + if hide_name_field: + make_property_setter( + doctype, fieldname, "hidden", 0, "Check", validate_fields_for_doctype=False + ) + make_property_setter(doctype, fieldname, "reqd", 1, "Check", validate_fields_for_doctype=False) + + # set values for mandatory + frappe.db.sql( + """update `tab{doctype}` set `{fieldname}`=`name` where + ifnull({fieldname}, '')=''""".format( + doctype=doctype, fieldname=fieldname + ) + ) From a62bc9b6c920b0d39a16b348a65962fa6a0d9992 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 31 May 2022 15:53:34 +0530 Subject: [PATCH 118/192] chore: Limit Update Cost jobs & `db_update` only if changed values - If `Update Cost` job is ongoing, then block creation of new ones since all BOMs are updated - `db_update` in `calculate_rm_cost` only if changed values to reduce redundant row updates - Misc: Use variable for batch size --- erpnext/manufacturing/doctype/bom/bom.py | 4 +++- .../doctype/bom_update_log/bom_update_log.py | 22 +++++++++++++++++-- .../bom_update_tool/bom_update_tool.py | 8 ++++++- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 7055efac28..d4e0d4b814 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -644,6 +644,7 @@ class BOM(WebsiteGenerator): base_total_rm_cost = 0 for d in self.get("items"): + old_rate = d.rate d.rate = self.get_rm_rate( { "company": self.company, @@ -656,6 +657,7 @@ class BOM(WebsiteGenerator): "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")) d.base_amount = d.amount * flt(self.conversion_rate) @@ -665,7 +667,7 @@ class BOM(WebsiteGenerator): total_rm_cost += d.amount base_total_rm_cost += d.base_amount - if save: + if save and (old_rate != d.rate): d.db_update() self.raw_material_cost = total_rm_cost diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index 639628ac38..f61f863c10 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -26,6 +26,8 @@ class BOMUpdateLog(Document): self.validate_boms_are_specified() self.validate_same_bom() self.validate_bom_items() + else: + self.validate_bom_cost_update_in_progress() self.status = "Queued" @@ -48,6 +50,21 @@ class BOMUpdateLog(Document): if current_bom_item != new_bom_item: frappe.throw(_("The selected BOMs are not for the same item")) + def validate_bom_cost_update_in_progress(self): + "If another Cost Updation Log is still in progress, dont make new ones." + + wip_log = frappe.get_all( + "BOM Update Log", + {"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress", "Paused"]]}, + limit_page_length=1, + ) + if wip_log: + log_link = frappe.utils.get_link_to_form("BOM Update Log", wip_log[0].name) + frappe.throw( + _("BOM Updation already in progress. Please wait until {0} is complete.").format(log_link), + title=_("Note"), + ) + def on_submit(self): if frappe.flags.in_test: return @@ -124,10 +141,11 @@ def queue_bom_cost_jobs(current_boms: Dict, update_doc: "BOMUpdateLog") -> None: current_boms_list = [bom for bom in current_boms] while current_boms_list: - boms_to_process = current_boms_list[:20000] # slice out batch of 20k BOMs + batch_size = 20_000 + boms_to_process = current_boms_list[:batch_size] # slice out batch of 20k BOMs # update list to exclude 20K (queued) BOMs - current_boms_list = current_boms_list[20000:] if len(current_boms_list) > 20000 else [] + current_boms_list = current_boms_list[batch_size:] if len(current_boms_list) > batch_size else [] frappe.enqueue( method="erpnext.manufacturing.doctype.bom_update_log.bom_updation_utils.update_cost_in_level", doc=update_doc, diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py index 4a2e03fb18..758d8ed0ef 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -38,7 +38,13 @@ def enqueue_update_cost() -> "BOMUpdateLog": def auto_update_latest_price_in_all_boms() -> None: """Called via hooks.py.""" if frappe.db.get_single_value("Manufacturing Settings", "update_bom_costs_automatically"): - create_bom_update_log(update_type="Update Cost") + wip_log = frappe.get_all( + "BOM Update Log", + {"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress", "Paused"]]}, + limit_page_length=1, + ) + if not wip_log: + create_bom_update_log(update_type="Update Cost") def create_bom_update_log( From 0331e37982b5513bc49ccfb8b840323bd9960041 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 31 May 2022 15:38:08 +0530 Subject: [PATCH 119/192] fix: incorrect billed_qty when item has multiple Delivery note sales order analysis report returns incorrect billed_qty value for an SO item has multiple delivery notes --- .../sales_order_analysis.py | 68 ++++++++++++++++--- 1 file changed, 57 insertions(+), 11 deletions(-) diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py index dcfb10a9d5..cc61594af4 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -1,11 +1,13 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt - import copy +from collections import OrderedDict import frappe -from frappe import _ +from frappe import _, qb +from frappe.query_builder import CustomFunction +from frappe.query_builder.functions import Max from frappe.utils import date_diff, flt, getdate @@ -18,11 +20,12 @@ def execute(filters=None): columns = get_columns(filters) conditions = get_conditions(filters) data = get_data(conditions, filters) + so_elapsed_time = get_so_elapsed_time(data) if not data: return [], [], None, [] - data, chart_data = prepare_data(data, filters) + data, chart_data = prepare_data(data, so_elapsed_time, filters) return columns, data, None, chart_data @@ -65,7 +68,6 @@ def get_data(conditions, filters): IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay, soi.qty, soi.delivered_qty, (soi.qty - soi.delivered_qty) AS pending_qty, - IF((SELECT pending_qty) = 0, (TO_SECONDS(Max(dn.posting_date))-TO_SECONDS(so.transaction_date)), 0) as time_taken_to_deliver, IFNULL(SUM(sii.qty), 0) as billed_qty, soi.base_amount as amount, (soi.delivered_qty * soi.base_rate) as delivered_qty_amount, @@ -76,13 +78,9 @@ def get_data(conditions, filters): soi.description as description FROM `tabSales Order` so, - (`tabSales Order Item` soi + `tabSales Order Item` soi LEFT JOIN `tabSales Invoice Item` sii - ON sii.so_detail = soi.name and sii.docstatus = 1) - LEFT JOIN `tabDelivery Note Item` dni - on dni.so_detail = soi.name - LEFT JOIN `tabDelivery Note` dn - on dni.parent = dn.name and dn.docstatus = 1 + ON sii.so_detail = soi.name and sii.docstatus = 1 WHERE soi.parent = so.name and so.status not in ('Stopped', 'Closed', 'On Hold') @@ -100,7 +98,48 @@ def get_data(conditions, filters): return data -def prepare_data(data, filters): +def get_so_elapsed_time(data): + """ + query SO's elapsed time till latest delivery note + """ + so_elapsed_time = OrderedDict() + if data: + sales_orders = [x.sales_order for x in data] + + so = qb.DocType("Sales Order") + soi = qb.DocType("Sales Order Item") + dn = qb.DocType("Delivery Note") + dni = qb.DocType("Delivery Note Item") + + to_seconds = CustomFunction("TO_SECONDS", ["date"]) + + query = ( + qb.from_(so) + .inner_join(soi) + .on(soi.parent == so.name) + .left_join(dni) + .on(dni.so_detail == soi.name) + .left_join(dn) + .on(dni.parent == dn.name) + .select( + so.name.as_("sales_order"), + soi.item_code.as_("so_item_code"), + (to_seconds(Max(dn.posting_date)) - to_seconds(so.transaction_date)).as_("elapsed_seconds"), + ) + .where((so.name.isin(sales_orders)) & (dn.docstatus == 1)) + .orderby(so.name, soi.name) + .groupby(soi.name) + ) + dn_elapsed_time = query.run(as_dict=True) + + for e in dn_elapsed_time: + key = (e.sales_order, e.so_item_code) + so_elapsed_time[key] = e.elapsed_seconds + + return so_elapsed_time + + +def prepare_data(data, so_elapsed_time, filters): completed, pending = 0, 0 if filters.get("group_by_so"): @@ -115,6 +154,13 @@ def prepare_data(data, filters): row["qty_to_bill"] = flt(row["qty"]) - flt(row["billed_qty"]) row["delay"] = 0 if row["delay"] and row["delay"] < 0 else row["delay"] + + row["time_taken_to_deliver"] = ( + so_elapsed_time.get((row.sales_order, row.item_code)) + if row["status"] in ("To Bill", "Completed") + else 0 + ) + if filters.get("group_by_so"): so_name = row["sales_order"] From 4f1bfbb93dc80f9f459d663cbe43433e47431ad5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 31 May 2022 12:10:04 +0530 Subject: [PATCH 120/192] test: multiple delivery notes and billed quantity --- .../test_sales_order_analysis.py | 106 ++++++++++++++++-- 1 file changed, 97 insertions(+), 9 deletions(-) diff --git a/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py index 25cbb73449..241f4358fb 100644 --- a/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py @@ -11,7 +11,7 @@ test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Delivery Note"] class TestSalesOrderAnalysis(FrappeTestCase): - def create_sales_order(self, transaction_date): + def create_sales_order(self, transaction_date, do_not_save=False, do_not_submit=False): item = create_item(item_code="_Test Excavator", is_stock_item=0) so = make_sales_order( transaction_date=transaction_date, @@ -24,25 +24,31 @@ class TestSalesOrderAnalysis(FrappeTestCase): so.taxes_and_charges = "" so.taxes = "" so.items[0].delivery_date = add_days(transaction_date, 15) - so.save() - so.submit() + if not do_not_save: + so.save() + if not do_not_submit: + so.submit() return item, so - def create_sales_invoice(self, so): + def create_sales_invoice(self, so, do_not_save=False, do_not_submit=False): sinv = make_sales_invoice(so.name) sinv.posting_date = so.transaction_date sinv.taxes_and_charges = "" sinv.taxes = "" - sinv.insert() - sinv.submit() + if not do_not_save: + sinv.save() + if not do_not_submit: + sinv.submit() return sinv - def create_delivery_note(self, so): + def create_delivery_note(self, so, do_not_save=False, do_not_submit=False): dn = make_delivery_note(so.name) dn.set_posting_time = True dn.posting_date = add_days(so.transaction_date, 1) - dn.save() - dn.submit() + if not do_not_save: + dn.save() + if not do_not_submit: + dn.submit() return dn def test_01_so_to_deliver_and_bill(self): @@ -164,3 +170,85 @@ class TestSalesOrderAnalysis(FrappeTestCase): ) # SO's from first 4 test cases should be in output self.assertEqual(len(data), 4) + + def test_06_so_pending_delivery_with_multiple_delivery_notes(self): + transaction_date = "2021-06-01" + item, so = self.create_sales_order(transaction_date) + + # bill 2 items + sinv1 = self.create_sales_invoice(so, do_not_save=True) + sinv1.items[0].qty = 2 + sinv1 = sinv1.save().submit() + # deliver 2 items + dn1 = self.create_delivery_note(so, do_not_save=True) + dn1.items[0].qty = 2 + dn1 = dn1.save().submit() + + # bill 2 items + sinv2 = self.create_sales_invoice(so, do_not_save=True) + sinv2.items[0].qty = 2 + sinv2 = sinv2.save().submit() + # deliver 1 item + dn2 = self.create_delivery_note(so, do_not_save=True) + dn2.items[0].qty = 1 + dn2 = dn2.save().submit() + + columns, data, message, chart = execute( + { + "company": "_Test Company", + "from_date": "2021-06-01", + "to_date": "2021-06-30", + "sales_order": [so.name], + } + ) + expected_value = { + "status": "To Deliver and Bill", + "sales_order": so.name, + "delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date), + "qty": 10, + "delivered_qty": 3, + "pending_qty": 7, + "qty_to_bill": 6, + "billed_qty": 4, + "time_taken_to_deliver": 0, + } + self.assertEqual(len(data), 1) + for key, val in expected_value.items(): + with self.subTest(key=key, val=val): + self.assertEqual(data[0][key], val) + + def test_07_so_delivered_with_multiple_delivery_notes(self): + transaction_date = "2021-06-01" + item, so = self.create_sales_order(transaction_date) + + dn1 = self.create_delivery_note(so, do_not_save=True) + dn1.items[0].qty = 5 + dn1 = dn1.save().submit() + + dn2 = self.create_delivery_note(so, do_not_save=True) + dn2.items[0].qty = 5 + dn2 = dn2.save().submit() + + columns, data, message, chart = execute( + { + "company": "_Test Company", + "from_date": "2021-06-01", + "to_date": "2021-06-30", + "sales_order": [so.name], + } + ) + expected_value = { + "status": "To Bill", + "sales_order": so.name, + "delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date), + "qty": 10, + "delivered_qty": 10, + "pending_qty": 0, + "qty_to_bill": 10, + "billed_qty": 0, + "time_taken_to_deliver": 86400, + } + self.assertEqual(len(data), 1) + for key, val in expected_value.items(): + with self.subTest(key=key, val=val): + self.assertEqual(data[0][key], val) From c4af63ad981649a2bcbc82c0fe79dd14e9cf2f47 Mon Sep 17 00:00:00 2001 From: hrzzz Date: Tue, 31 May 2022 10:46:56 -0300 Subject: [PATCH 121/192] feat: two new groupby mode: Monthly, Payment Term --- .../report/gross_profit/gross_profit.js | 2 +- .../report/gross_profit/gross_profit.py | 103 ++++++++++++++++-- 2 files changed, 92 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js index 158ff4d343..3d37b5898c 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.js +++ b/erpnext/accounts/report/gross_profit/gross_profit.js @@ -35,7 +35,7 @@ frappe.query_reports["Gross Profit"] = { "fieldname":"group_by", "label": __("Group By"), "fieldtype": "Select", - "options": "Invoice\nItem Code\nItem Group\nBrand\nWarehouse\nCustomer\nCustomer Group\nTerritory\nSales Person\nProject", + "options": "Invoice\nItem Code\nItem Group\nBrand\nWarehouse\nCustomer\nCustomer Group\nTerritory\nSales Person\nProject\nMonthly\nPayment Term", "default": "Invoice" }, ], diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 9668992e02..526ea9d6e2 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -4,7 +4,7 @@ import frappe from frappe import _, scrub -from frappe.utils import cint, flt +from frappe.utils import cint, flt, formatdate from erpnext.controllers.queries import get_match_cond from erpnext.stock.utils import get_incoming_rate @@ -124,6 +124,23 @@ def execute(filters=None): "gross_profit", "gross_profit_percent", ], + "monthly": [ + "monthly", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "payment_term": [ + "payment_term", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], } ) @@ -317,6 +334,19 @@ def get_columns(group_wise_columns, filters): "options": "territory", "width": 100, }, + "monthly": { + "label": _("Monthly"), + "fieldname": "monthly", + "fieldtype": "Data", + "width": 100, + }, + "payment_term": { + "label": _("Payment Term"), + "fieldname": "payment_term", + "fieldtype": "Link", + "options": "Payment Term", + "width": 170, + }, } ) @@ -390,6 +420,9 @@ class GrossProfitGenerator(object): buying_amount = 0 for row in reversed(self.si_list): + if self.filters.get("group_by") == "Monthly": + row.monthly = formatdate(row.posting_date, "MMM YYYY") + if self.skip_row(row): continue @@ -445,17 +478,7 @@ class GrossProfitGenerator(object): def get_average_rate_based_on_group_by(self): for key in list(self.grouped): - if self.filters.get("group_by") != "Invoice": - for i, row in enumerate(self.grouped[key]): - if i == 0: - new_row = row - else: - new_row.qty += flt(row.qty) - new_row.buying_amount += flt(row.buying_amount, self.currency_precision) - new_row.base_amount += flt(row.base_amount, self.currency_precision) - new_row = self.set_average_rate(new_row) - self.grouped_data.append(new_row) - else: + if self.filters.get("group_by") == "Invoice": for i, row in enumerate(self.grouped[key]): if row.indent == 1.0: if ( @@ -469,6 +492,44 @@ class GrossProfitGenerator(object): if flt(row.qty) or row.base_amount: row = self.set_average_rate(row) self.grouped_data.append(row) + elif self.filters.get("group_by") == "Payment Term": + for i, row in enumerate(self.grouped[key]): + invoice_portion = 0 + + if row.is_return: + invoice_portion = 100 + elif row.invoice_portion: + invoice_portion = row.invoice_portion + else: + invoice_portion = row.payment_amount * 100 / row.base_net_amount + + if i == 0: + new_row = row + self.set_average_based_on_payment_term_portion(new_row, row, invoice_portion) + else: + new_row.qty += flt(row.qty) + self.set_average_based_on_payment_term_portion(new_row, row, invoice_portion, True) + + new_row = self.set_average_rate(new_row) + self.grouped_data.append(new_row) + else: + for i, row in enumerate(self.grouped[key]): + if i == 0: + new_row = row + else: + new_row.qty += flt(row.qty) + new_row.buying_amount += flt(row.buying_amount, self.currency_precision) + new_row.base_amount += flt(row.base_amount, self.currency_precision) + new_row = self.set_average_rate(new_row) + self.grouped_data.append(new_row) + + def set_average_based_on_payment_term_portion(self, new_row, row, invoice_portion, aggr=False): + cols = ["base_amount", "buying_amount", "gross_profit"] + for col in cols: + if aggr: + new_row[col] += row[col] * invoice_portion / 100 + else: + new_row[col] = row[col] * invoice_portion / 100 def is_not_invoice_row(self, row): return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get( @@ -622,6 +683,20 @@ class GrossProfitGenerator(object): sales_person_cols = "" sales_team_table = "" + if self.filters.group_by == "Payment Term": + payment_term_cols = """,if(`tabSales Invoice`.is_return = 1, + '{0}', + coalesce(schedule.payment_term, '{1}')) as payment_term, + schedule.invoice_portion, + schedule.payment_amount """.format( + _("Sales Return"), _("No Terms") + ) + payment_term_table = """ left join `tabPayment Schedule` schedule on schedule.parent = `tabSales Invoice`.name and + `tabSales Invoice`.is_return = 0 """ + else: + payment_term_cols = "" + payment_term_table = "" + if self.filters.get("sales_invoice"): conditions += " and `tabSales Invoice`.name = %(sales_invoice)s" @@ -644,10 +719,12 @@ class GrossProfitGenerator(object): `tabSales Invoice Item`.name as "item_row", `tabSales Invoice`.is_return, `tabSales Invoice Item`.cost_center {sales_person_cols} + {payment_term_cols} from `tabSales Invoice` inner join `tabSales Invoice Item` on `tabSales Invoice Item`.parent = `tabSales Invoice`.name {sales_team_table} + {payment_term_table} where `tabSales Invoice`.docstatus=1 and `tabSales Invoice`.is_opening!='Yes' {conditions} {match_cond} order by @@ -655,6 +732,8 @@ class GrossProfitGenerator(object): conditions=conditions, sales_person_cols=sales_person_cols, sales_team_table=sales_team_table, + payment_term_cols=payment_term_cols, + payment_term_table=payment_term_table, match_cond=get_match_cond("Sales Invoice"), ), self.filters, From a6beafbc3c33643a1ba6c35ab52b5b9edc125d7e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 31 May 2022 19:41:46 +0530 Subject: [PATCH 122/192] fix: Permission for selling and buying settings --- .../doctype/buying_settings/buying_settings.json | 12 +++++++++++- .../doctype/selling_settings/selling_settings.json | 11 ++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 89a9448716..6c18a4650b 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -148,7 +148,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-04-14 15:56:42.340223", + "modified": "2022-05-31 19:40:26.103909", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", @@ -162,6 +162,16 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "role": "Purchase Manager", + "share": 1, + "write": 1 } ], "sort_field": "modified", diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 005e24cfbe..2abb169b8a 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -179,7 +179,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-04-14 16:01:29.405642", + "modified": "2022-05-31 19:39:48.398738", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", @@ -193,6 +193,15 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "Sales Manager", + "share": 1, + "write": 1 } ], "sort_field": "modified", From b3ccc4bfb953b90dc8301a6af953c1a2cd66d4b6 Mon Sep 17 00:00:00 2001 From: maharshivpatel <39730881+maharshivpatel@users.noreply.github.com> Date: Tue, 31 May 2022 19:48:30 +0530 Subject: [PATCH 123/192] fix: Auto Insert Item Price If Missing when discount & blank UOM (#31168) * fix: Auto Insert Item Price If Missing when discount and blank UOM fixes wrong item price insert when discount is used and adds uom=stock_uom instead of blank as price is converted to stock uom * unit tests added for item with discount I have added test for auto_insert_price where discount is used. * unit test issue fixed fixed make_sales_order as some of the test that depended on it were failing due to passing of incorrect parameters. Co-authored-by: Ankush Menat --- .../doctype/sales_order/test_sales_order.py | 24 ++++++++++++++++++- erpnext/stock/get_item_details.py | 8 +++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index acae37f547..96308f0bee 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -783,6 +783,7 @@ class TestSalesOrder(FrappeTestCase): def test_auto_insert_price(self): make_item("_Test Item for Auto Price List", {"is_stock_item": 0}) + make_item("_Test Item for Auto Price List with Discount Percentage", {"is_stock_item": 0}) frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 1) item_price = frappe.db.get_value( @@ -804,6 +805,25 @@ class TestSalesOrder(FrappeTestCase): 100, ) + make_sales_order( + item_code="_Test Item for Auto Price List with Discount Percentage", + selling_price_list="_Test Price List", + price_list_rate=200, + discount_percentage=20, + ) + + self.assertEqual( + frappe.db.get_value( + "Item Price", + { + "price_list": "_Test Price List", + "item_code": "_Test Item for Auto Price List with Discount Percentage", + }, + "price_list_rate", + ), + 200, + ) + # do not update price list frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0) @@ -1659,7 +1679,9 @@ def make_sales_order(**args): "warehouse": args.warehouse, "qty": args.qty or 10, "uom": args.uom or None, - "rate": args.rate or 100, + "price_list_rate": args.price_list_rate or None, + "discount_percentage": args.discount_percentage or None, + "rate": args.rate or (None if args.price_list_rate else 100), "against_blanket_order": args.against_blanket_order, }, ) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index c6241f8df6..c8d9f5404f 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -353,6 +353,7 @@ def get_basic_details(args, item, overwrite_warehouse=True): "has_batch_no": item.has_batch_no, "batch_no": args.get("batch_no"), "uom": args.uom, + "stock_uom": item.stock_uom, "min_order_qty": flt(item.min_order_qty) if args.doctype == "Material Request" else "", "qty": flt(args.qty) or 1.0, "stock_qty": flt(args.qty) or 1.0, @@ -365,7 +366,7 @@ def get_basic_details(args, item, overwrite_warehouse=True): "net_rate": 0.0, "net_amount": 0.0, "discount_percentage": 0.0, - "discount_amount": 0.0, + "discount_amount": flt(args.discount_amount) or 0.0, "supplier": get_default_supplier(args, item_defaults, item_group_defaults, brand_defaults), "update_stock": args.get("update_stock") if args.get("doctype") in ["Sales Invoice", "Purchase Invoice"] @@ -823,7 +824,9 @@ def insert_item_price(args): ): if frappe.has_permission("Item Price", "write"): price_list_rate = ( - args.rate / args.get("conversion_factor") if args.get("conversion_factor") else args.rate + (args.rate + args.discount_amount) / args.get("conversion_factor") + if args.get("conversion_factor") + else (args.rate + args.discount_amount) ) item_price = frappe.db.get_value( @@ -849,6 +852,7 @@ def insert_item_price(args): "item_code": args.item_code, "currency": args.currency, "price_list_rate": price_list_rate, + "uom": args.stock_uom, } ) item_price.insert() From 167cf7b49d9b9b3b531459519cd73f53b8fcd742 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 1 Jun 2022 09:04:53 +0530 Subject: [PATCH 124/192] fix: Remove domain restrcition from Manufacturing Workspace --- .../manufacturing/workspace/manufacturing/manufacturing.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json index 05ca2a8452..9829a96e09 100644 --- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json +++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json @@ -402,14 +402,15 @@ "type": "Link" } ], - "modified": "2022-01-13 17:40:09.474747", + "modified": "2022-05-31 22:08:19.408223", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing", "owner": "Administrator", "parent_page": "", "public": 1, - "restrict_to_domain": "Manufacturing", + "quick_lists": [], + "restrict_to_domain": "", "roles": [], "sequence_id": 17.0, "shortcuts": [ From 653d6341d46fcc6e2c49167a0ecfc7ccb1003d35 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 1 Jun 2022 12:14:42 +0530 Subject: [PATCH 125/192] refactor: clean-up and commonify payroll entry test setups --- .../payroll_entry/test_payroll_entry.py | 295 ++++++++---------- 1 file changed, 126 insertions(+), 169 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index 5c68bd35ef..47b9962912 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -25,7 +25,6 @@ from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( create_account, make_deduction_salary_component, make_earning_salary_component, - make_employee_salary_slip, set_salary_component_account, ) from erpnext.payroll.doctype.salary_structure.test_salary_structure import ( @@ -38,10 +37,6 @@ test_dependencies = ["Holiday List"] class TestPayrollEntry(FrappeTestCase): def setUp(self): - frappe.db.set_value( - "Company", erpnext.get_default_company(), "default_holiday_list", "_Test Holiday List" - ) - for dt in [ "Salary Slip", "Salary Component", @@ -51,76 +46,79 @@ class TestPayrollEntry(FrappeTestCase): "Salary Structure Assignment", "Payroll Employee Detail", "Additional Salary", + "Loan", ]: - frappe.db.sql("delete from `tab%s`" % dt) + frappe.db.delete(dt) make_earning_salary_component(setup=True, company_list=["_Test Company"]) make_deduction_salary_component(setup=True, test_tax=False, company_list=["_Test Company"]) + frappe.db.set_value("Company", "_Test Company", "default_holiday_list", "_Test Holiday List") frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 0) - def test_payroll_entry(self): # pylint: disable=no-self-use + # set default payable account + default_account = frappe.db.get_value( + "Company", "_Test Company", "default_payroll_payable_account" + ) + if not default_account or default_account != "_Test Payroll Payable - _TC": + create_account( + account_name="_Test Payroll Payable", + company="_Test Company", + parent_account="Current Liabilities - _TC", + account_type="Payable", + ) + frappe.db.set_value( + "Company", "_Test Company", "default_payroll_payable_account", "_Test Payroll Payable - _TC" + ) + + def test_payroll_entry(self): + company = frappe.get_doc("Company", "_Test Company") + employee = frappe.db.get_value("Employee", {"company": "_Test Company"}) + setup_salary_structure(employee, company) + + dates = get_start_end_dates("Monthly", nowdate()) + make_payroll_entry( + start_date=dates.start_date, + end_date=dates.end_date, + payable_account=company.default_payroll_payable_account, + currency=company.default_currency, + company=company.name, + ) + + def test_multi_currency_payroll_entry(self): company = erpnext.get_default_company() + employee = make_employee("test_muti_currency_employee@payroll.com", company=company) for data in frappe.get_all("Salary Component", fields=["name"]): if not frappe.db.get_value( "Salary Component Account", {"parent": data.name, "company": company}, "name" ): - set_salary_component_account(data.name) + get_salary_component_account(data.name) - employee = frappe.db.get_value("Employee", {"company": company}) company_doc = frappe.get_doc("Company", company) - make_salary_structure( - "_Test Salary Structure", - "Monthly", - employee, - company=company, - currency=company_doc.default_currency, + salary_structure = make_salary_structure( + "_Test Multi Currency Salary Structure", "Monthly", company=company, currency="USD" ) - dates = get_start_end_dates("Monthly", nowdate()) - if not frappe.db.get_value( - "Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date} - ): - make_payroll_entry( - start_date=dates.start_date, - end_date=dates.end_date, - payable_account=company_doc.default_payroll_payable_account, - currency=company_doc.default_currency, - ) - - def test_multi_currency_payroll_entry(self): - company = frappe.get_doc("Company", "_Test Company") - employee = make_employee( - "test_muti_currency_employee@payroll.com", company=company.name, department="Accounts - _TC" + create_salary_structure_assignment( + employee, salary_structure.name, company=company, currency="USD" ) - - for data in frappe.get_all("Salary Component", fields=["name"]): - if not frappe.db.get_value( - "Salary Component Account", {"parent": data.name, "company": company.name}, "name" - ): - set_salary_component_account(data.name) - - salary_struct = make_salary_structure( - "_Test Multi Currency Salary Structure", - "Monthly", - employee, - currency="USD", - company=company.name, + frappe.db.sql( + """delete from `tabSalary Slip` where employee=%s""", + (frappe.db.get_value("Employee", {"user_id": "test_muti_currency_employee@payroll.com"})), + ) + salary_slip = get_salary_slip( + "test_muti_currency_employee@payroll.com", "Monthly", "_Test Multi Currency Salary Structure" ) - - frappe.db.delete("Salary Slip", {"employee": employee}) dates = get_start_end_dates("Monthly", nowdate()) payroll_entry = make_payroll_entry( start_date=dates.start_date, end_date=dates.end_date, - payable_account=company.default_payroll_payable_account, + payable_account=company_doc.default_payroll_payable_account, currency="USD", exchange_rate=70, - company=company.name, ) payroll_entry.make_payment_entry() - salary_slip = frappe.db.get_value("Salary Slip", {"payroll_entry": payroll_entry.name}) - salary_slip = frappe.get_doc("Salary Slip", salary_slip) + salary_slip.load_from_db() payroll_je = salary_slip.journal_entry if payroll_je: @@ -143,22 +141,11 @@ class TestPayrollEntry(FrappeTestCase): self.assertEqual(salary_slip.base_net_pay, payment_entry[0].total_credit) def test_payroll_entry_with_employee_cost_center(self): - for data in frappe.get_all("Salary Component", fields=["name"]): - if not frappe.db.get_value( - "Salary Component Account", {"parent": data.name, "company": "_Test Company"}, "name" - ): - set_salary_component_account(data.name) - if not frappe.db.exists("Department", "cc - _TC"): frappe.get_doc( {"doctype": "Department", "department_name": "cc", "company": "_Test Company"} ).insert() - frappe.db.sql("""delete from `tabEmployee` where employee_name='test_employee1@example.com' """) - frappe.db.sql("""delete from `tabEmployee` where employee_name='test_employee2@example.com' """) - frappe.db.sql("""delete from `tabSalary Structure` where name='_Test Salary Structure 1' """) - frappe.db.sql("""delete from `tabSalary Structure` where name='_Test Salary Structure 2' """) - employee1 = make_employee( "test_employee1@example.com", payroll_cost_center="_Test Cost Center - _TC", @@ -169,38 +156,15 @@ class TestPayrollEntry(FrappeTestCase): "test_employee2@example.com", department="cc - _TC", company="_Test Company" ) - if not frappe.db.exists("Account", "_Test Payroll Payable - _TC"): - create_account( - account_name="_Test Payroll Payable", - company="_Test Company", - parent_account="Current Liabilities - _TC", - account_type="Payable", - ) + company = frappe.get_doc("Company", "_Test Company") + setup_salary_structure(employee1, company) - if ( - not frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") - or frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") - != "_Test Payroll Payable - _TC" - ): - frappe.db.set_value( - "Company", "_Test Company", "default_payroll_payable_account", "_Test Payroll Payable - _TC" - ) - currency = frappe.db.get_value("Company", "_Test Company", "default_currency") - - make_salary_structure( - "_Test Salary Structure 1", - "Monthly", - employee1, - company="_Test Company", - currency=currency, - test_tax=False, - ) ss = make_salary_structure( "_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", - currency=currency, + currency=company.default_currency, test_tax=False, ) @@ -219,42 +183,38 @@ class TestPayrollEntry(FrappeTestCase): ssa_doc.append( "payroll_cost_centers", {"cost_center": "_Test Cost Center 2 - _TC", "percentage": 40} ) - ssa_doc.save() dates = get_start_end_dates("Monthly", nowdate()) - if not frappe.db.get_value( - "Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date} - ): - pe = make_payroll_entry( - start_date=dates.start_date, - end_date=dates.end_date, - payable_account="_Test Payroll Payable - _TC", - currency=frappe.db.get_value("Company", "_Test Company", "default_currency"), - department="cc - _TC", - company="_Test Company", - payment_account="Cash - _TC", - cost_center="Main - _TC", - ) - je = frappe.db.get_value("Salary Slip", {"payroll_entry": pe.name}, "journal_entry") - je_entries = frappe.db.sql( - """ - select account, cost_center, debit, credit - from `tabJournal Entry Account` - where parent=%s - order by account, cost_center - """, - je, - ) - expected_je = ( - ("_Test Payroll Payable - _TC", "Main - _TC", 0.0, 155600.0), - ("Salary - _TC", "_Test Cost Center - _TC", 124800.0, 0.0), - ("Salary - _TC", "_Test Cost Center 2 - _TC", 31200.0, 0.0), - ("Salary Deductions - _TC", "_Test Cost Center - _TC", 0.0, 320.0), - ("Salary Deductions - _TC", "_Test Cost Center 2 - _TC", 0.0, 80.0), - ) + pe = make_payroll_entry( + start_date=dates.start_date, + end_date=dates.end_date, + payable_account="_Test Payroll Payable - _TC", + currency=frappe.db.get_value("Company", "_Test Company", "default_currency"), + department="cc - _TC", + company="_Test Company", + payment_account="Cash - _TC", + cost_center="Main - _TC", + ) + je = frappe.db.get_value("Salary Slip", {"payroll_entry": pe.name}, "journal_entry") + je_entries = frappe.db.sql( + """ + select account, cost_center, debit, credit + from `tabJournal Entry Account` + where parent=%s + order by account, cost_center + """, + je, + ) + expected_je = ( + ("_Test Payroll Payable - _TC", "Main - _TC", 0.0, 155600.0), + ("Salary - _TC", "_Test Cost Center - _TC", 124800.0, 0.0), + ("Salary - _TC", "_Test Cost Center 2 - _TC", 31200.0, 0.0), + ("Salary Deductions - _TC", "_Test Cost Center - _TC", 0.0, 320.0), + ("Salary Deductions - _TC", "_Test Cost Center 2 - _TC", 0.0, 80.0), + ) - self.assertEqual(je_entries, expected_je) + self.assertEqual(je_entries, expected_je) def test_get_end_date(self): self.assertEqual(get_end_date("2017-01-01", "monthly"), {"end_date": "2017-01-31"}) @@ -267,31 +227,22 @@ class TestPayrollEntry(FrappeTestCase): self.assertEqual(get_end_date("2017-02-15", "daily"), {"end_date": "2017-02-15"}) def test_loan(self): - branch = "Test Employee Branch" - applicant = make_employee("test_employee@loan.com", company="_Test Company") company = "_Test Company" - holiday_list = make_holiday("test holiday for loan") - - company_doc = frappe.get_doc("Company", company) - if not company_doc.default_payroll_payable_account: - company_doc.default_payroll_payable_account = frappe.db.get_value( - "Account", {"company": company, "root_type": "Liability", "account_type": ""}, "name" - ) - company_doc.save() + branch = "Test Employee Branch" if not frappe.db.exists("Branch", branch): frappe.get_doc({"doctype": "Branch", "branch": branch}).insert() + holiday_list = make_holiday("test holiday for loan") - employee_doc = frappe.get_doc("Employee", applicant) - employee_doc.branch = branch - employee_doc.holiday_list = holiday_list - employee_doc.save() + applicant = make_employee( + "test_employee@loan.com", company="_Test Company", branch=branch, holiday_list=holiday_list + ) + company_doc = frappe.get_doc("Company", company) - salary_structure = "Test Salary Structure for Loan" make_salary_structure( - salary_structure, + "Test Salary Structure for Loan", "Monthly", - employee=employee_doc.name, + employee=applicant, company="_Test Company", currency=company_doc.default_currency, ) @@ -352,21 +303,11 @@ class TestPayrollEntry(FrappeTestCase): self.assertEqual(row.principal_amount, principal_amount) self.assertEqual(row.total_payment, interest_amount + principal_amount) - if salary_slip.docstatus == 0: - frappe.delete_doc("Salary Slip", name) - def test_salary_slip_operation_queueing(self): - # setup - company = erpnext.get_default_company() + company = "_Test Company" company_doc = frappe.get_doc("Company", company) employee = frappe.db.get_value("Employee", {"company": company}) - make_salary_structure( - "_Test Salary Structure", - "Monthly", - employee, - company=company, - currency=company_doc.default_currency, - ) + setup_salary_structure(employee, company_doc) # enqueue salary slip creation via payroll entry # Payroll Entry status should change to Queued @@ -376,6 +317,7 @@ class TestPayrollEntry(FrappeTestCase): end_date=dates.end_date, payable_account=company_doc.default_payroll_payable_account, currency=company_doc.default_currency, + company=company_doc.name, ) frappe.flags.enqueue_payroll_entry = True payroll_entry.create_salary_slips() @@ -385,10 +327,10 @@ class TestPayrollEntry(FrappeTestCase): frappe.flags.enqueue_payroll_entry = False def test_salary_slip_operation_failure(self): - # setup - company = erpnext.get_default_company() + company = "_Test Company" company_doc = frappe.get_doc("Company", company) employee = frappe.db.get_value("Employee", {"company": company}) + salary_structure = make_salary_structure( "_Test Salary Structure", "Monthly", @@ -410,6 +352,7 @@ class TestPayrollEntry(FrappeTestCase): end_date=dates.end_date, payable_account=company_doc.default_payroll_payable_account, currency=company_doc.default_currency, + company=company_doc.name, ) payroll_entry.create_salary_slips() payroll_entry.submit_salary_slips() @@ -419,41 +362,30 @@ class TestPayrollEntry(FrappeTestCase): self.assertIsNotNone(payroll_entry.error_message) # set accounts - for data in frappe.get_all("Salary Component", fields=["name"]): - if not frappe.db.get_value( - "Salary Component Account", {"parent": data.name, "company": company}, "name" - ): - set_salary_component_account(data.name, company_list=[company]) + for data in frappe.get_all("Salary Component", pluck="name"): + set_salary_component_account(data, company_list=[company]) # Payroll Entry successful, status should change to Submitted payroll_entry.submit_salary_slips() payroll_entry.reload() + self.assertEqual(payroll_entry.status, "Submitted") self.assertEqual(payroll_entry.error_message, "") def test_payroll_entry_status(self): - company = erpnext.get_default_company() - for data in frappe.get_all("Salary Component", fields=["name"]): - if not frappe.db.get_value( - "Salary Component Account", {"parent": data.name, "company": company}, "name" - ): - set_salary_component_account(data.name) - - employee = frappe.db.get_value("Employee", {"company": company}) + company = "_Test Company" company_doc = frappe.get_doc("Company", company) - make_salary_structure( - "_Test Salary Structure", - "Monthly", - employee, - company=company, - currency=company_doc.default_currency, - ) + employee = frappe.db.get_value("Employee", {"company": company}) + + setup_salary_structure(employee, company_doc) + dates = get_start_end_dates("Monthly", nowdate()) payroll_entry = get_payroll_entry_data( start_date=dates.start_date, end_date=dates.end_date, payable_account=company_doc.default_payroll_payable_account, currency=company_doc.default_currency, + company=company_doc.name, ) payroll_entry.submit() self.assertEqual(payroll_entry.status, "Submitted") @@ -532,3 +464,28 @@ def make_holiday(holiday_list_name): ).insert() return holiday_list_name + + +def get_salary_slip(user, period, salary_structure): + salary_slip = make_employee_salary_slip(user, period, salary_structure) + salary_slip.exchange_rate = 70 + salary_slip.calculate_net_pay() + salary_slip.db_update() + + return salary_slip + + +def setup_salary_structure(employee, company_doc, currency=None, salary_structure=None): + for data in frappe.get_all("Salary Component", pluck="name"): + if not frappe.db.get_value( + "Salary Component Account", {"parent": data, "company": company_doc.name}, "name" + ): + set_salary_component_account(data) + + make_salary_structure( + salary_structure or "_Test Salary Structure", + "Monthly", + employee, + company=company_doc.name, + currency=(currency or company_doc.default_currency), + ) From d4b9cc02420fff8310f547b55ae158c717ec8fb0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 1 Jun 2022 12:18:14 +0530 Subject: [PATCH 126/192] fix: remove leave policy assignment creation patch (#31097) --- erpnext/patches.txt | 1 - ..._based_on_employee_current_leave_policy.py | 94 ------------------- 2 files changed, 95 deletions(-) delete mode 100644 erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 785e2baa11..ad1ba2a157 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -231,7 +231,6 @@ erpnext.patches.v13_0.updates_for_multi_currency_payroll erpnext.patches.v13_0.update_reason_for_resignation_in_employee execute:frappe.delete_doc("Report", "Quoted Item Comparison") erpnext.patches.v13_0.update_member_email_address -erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy erpnext.patches.v13_0.update_pos_closing_entry_in_merge_log erpnext.patches.v13_0.add_po_to_global_search erpnext.patches.v13_0.update_returned_qty_in_pr_dn diff --git a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py deleted file mode 100644 index 59b17eea9f..0000000000 --- a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (c) 2019, Frappe and Contributors -# License: GNU General Public License v3. See license.txt - - -import frappe - - -def execute(): - frappe.reload_doc("hr", "doctype", "leave_policy_assignment") - frappe.reload_doc("hr", "doctype", "employee_grade") - employee_with_assignment = [] - leave_policy = [] - - if "leave_policy" in frappe.db.get_table_columns("Employee"): - employees_with_leave_policy = frappe.db.sql( - "SELECT name, leave_policy FROM `tabEmployee` WHERE leave_policy IS NOT NULL and leave_policy != ''", - as_dict=1, - ) - - for employee in employees_with_leave_policy: - alloc = frappe.db.exists( - "Leave Allocation", - {"employee": employee.name, "leave_policy": employee.leave_policy, "docstatus": 1}, - ) - if not alloc: - create_assignment(employee.name, employee.leave_policy) - - employee_with_assignment.append(employee.name) - leave_policy.append(employee.leave_policy) - - if "default_leave_policy" in frappe.db.get_table_columns("Employee Grade"): - employee_grade_with_leave_policy = frappe.db.sql( - "SELECT name, default_leave_policy FROM `tabEmployee Grade` WHERE default_leave_policy IS NOT NULL and default_leave_policy!=''", - as_dict=1, - ) - - # for whole employee Grade - for grade in employee_grade_with_leave_policy: - employees = get_employee_with_grade(grade.name) - for employee in employees: - - if employee not in employee_with_assignment: # Will ensure no duplicate - alloc = frappe.db.exists( - "Leave Allocation", - {"employee": employee.name, "leave_policy": grade.default_leave_policy, "docstatus": 1}, - ) - if not alloc: - create_assignment(employee.name, grade.default_leave_policy) - leave_policy.append(grade.default_leave_policy) - - # for old Leave allocation and leave policy from allocation, which may got updated in employee grade. - leave_allocations = frappe.db.sql( - "SELECT leave_policy, leave_period, employee FROM `tabLeave Allocation` WHERE leave_policy IS NOT NULL and leave_policy != '' and docstatus = 1 ", - as_dict=1, - ) - - for allocation in leave_allocations: - if allocation.leave_policy not in leave_policy: - create_assignment( - allocation.employee, - allocation.leave_policy, - leave_period=allocation.leave_period, - allocation_exists=True, - ) - - -def create_assignment(employee, leave_policy, leave_period=None, allocation_exists=False): - if frappe.db.get_value("Leave Policy", leave_policy, "docstatus") == 2: - return - - filters = {"employee": employee, "leave_policy": leave_policy} - if leave_period: - filters["leave_period"] = leave_period - - if not frappe.db.exists("Leave Policy Assignment", filters): - lpa = frappe.new_doc("Leave Policy Assignment") - lpa.employee = employee - lpa.leave_policy = leave_policy - - lpa.flags.ignore_mandatory = True - if allocation_exists: - lpa.assignment_based_on = "Leave Period" - lpa.leave_period = leave_period - lpa.leaves_allocated = 1 - - lpa.save() - if allocation_exists: - lpa.submit() - # Updating old Leave Allocation - frappe.db.sql("Update `tabLeave Allocation` set leave_policy_assignment = %s", lpa.name) - - -def get_employee_with_grade(grade): - return frappe.get_list("Employee", filters={"grade": grade}) From 03a24ce774ad79c928ba30944ad014c3b0617e8b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 1 Jun 2022 12:33:23 +0530 Subject: [PATCH 127/192] chore: delete dead cypress code Moved to separate repo --- cypress.json | 11 - cypress/fixtures/example.json | 5 - .../test_bulk_transaction_processing.js | 44 ---- cypress/integration/test_customer.js | 13 -- cypress/integration/test_item.js | 44 ---- .../test_organizational_chart_desktop.js | 116 ----------- .../test_organizational_chart_mobile.js | 195 ------------------ cypress/plugins/index.js | 17 -- cypress/support/commands.js | 31 --- cypress/support/index.js | 26 --- cypress/tsconfig.json | 12 -- 11 files changed, 514 deletions(-) delete mode 100644 cypress.json delete mode 100644 cypress/fixtures/example.json delete mode 100644 cypress/integration/test_bulk_transaction_processing.js delete mode 100644 cypress/integration/test_customer.js delete mode 100644 cypress/integration/test_item.js delete mode 100644 cypress/integration/test_organizational_chart_desktop.js delete mode 100644 cypress/integration/test_organizational_chart_mobile.js delete mode 100644 cypress/plugins/index.js delete mode 100644 cypress/support/commands.js delete mode 100644 cypress/support/index.js delete mode 100644 cypress/tsconfig.json diff --git a/cypress.json b/cypress.json deleted file mode 100644 index 02b10d893f..0000000000 --- a/cypress.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "baseUrl": "http://test_site:8000/", - "projectId": "da59y9", - "adminPassword": "admin", - "defaultCommandTimeout": 20000, - "pageLoadTimeout": 15000, - "retries": { - "runMode": 2, - "openMode": 2 - } -} diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json deleted file mode 100644 index da18d9352a..0000000000 --- a/cypress/fixtures/example.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Using fixtures to represent data", - "email": "hello@cypress.io", - "body": "Fixtures are a great way to mock data for responses to routes" -} \ No newline at end of file diff --git a/cypress/integration/test_bulk_transaction_processing.js b/cypress/integration/test_bulk_transaction_processing.js deleted file mode 100644 index 428ec5100b..0000000000 --- a/cypress/integration/test_bulk_transaction_processing.js +++ /dev/null @@ -1,44 +0,0 @@ -describe("Bulk Transaction Processing", () => { - before(() => { - cy.login(); - cy.visit("/app/website"); - }); - - it("Creates To Sales Order", () => { - cy.visit("/app/sales-order"); - cy.url().should("include", "/sales-order"); - cy.window() - .its("frappe.csrf_token") - .then((csrf_token) => { - return cy - .request({ - url: "/api/method/erpnext.tests.ui_test_bulk_transaction_processing.create_records", - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - "X-Frappe-CSRF-Token": csrf_token, - }, - timeout: 60000, - }) - .then((res) => { - expect(res.status).eq(200); - }); - }); - cy.wait(5000); - cy.get( - ".list-row-head > .list-header-subject > .list-row-col > .list-check-all" - ).check({ force: true }); - cy.wait(3000); - cy.get(".actions-btn-group > .btn-primary").click({ force: true }); - cy.wait(3000); - cy.get(".dropdown-menu-right > .user-action > .dropdown-item") - .contains("Sales Invoice") - .click({ force: true }); - cy.wait(3000); - cy.get(".modal-content > .modal-footer > .standard-actions") - .contains("Yes") - .click({ force: true }); - cy.contains("Creation of Sales Invoice successful"); - }); -}); diff --git a/cypress/integration/test_customer.js b/cypress/integration/test_customer.js deleted file mode 100644 index 3d6ed5d0d8..0000000000 --- a/cypress/integration/test_customer.js +++ /dev/null @@ -1,13 +0,0 @@ - -context('Customer', () => { - before(() => { - cy.login(); - }); - it('Check Customer Group', () => { - cy.visit(`app/customer/`); - cy.get('.primary-action').click(); - cy.wait(500); - cy.get('.custom-actions > .btn').click(); - cy.get_field('customer_group', 'Link').should('have.value', 'All Customer Groups'); - }); -}); diff --git a/cypress/integration/test_item.js b/cypress/integration/test_item.js deleted file mode 100644 index fcb7533a22..0000000000 --- a/cypress/integration/test_item.js +++ /dev/null @@ -1,44 +0,0 @@ -describe("Test Item Dashboard", () => { - before(() => { - cy.login(); - cy.visit("/app/item"); - cy.insert_doc( - "Item", - { - item_code: "e2e_test_item", - item_group: "All Item Groups", - opening_stock: 42, - valuation_rate: 100, - }, - true - ); - cy.go_to_doc("item", "e2e_test_item"); - }); - - it("should show dashboard with correct data on first load", () => { - cy.get(".stock-levels").contains("Stock Levels").should("be.visible"); - cy.get(".stock-levels").contains("e2e_test_item").should("exist"); - - // reserved and available qty - cy.get(".stock-levels .inline-graph-count") - .eq(0) - .contains("0") - .should("exist"); - cy.get(".stock-levels .inline-graph-count") - .eq(1) - .contains("42") - .should("exist"); - }); - - it("should persist on field change", () => { - cy.get('input[data-fieldname="disabled"]').check(); - cy.wait(500); - cy.get(".stock-levels").contains("Stock Levels").should("be.visible"); - cy.get(".stock-levels").should("have.length", 1); - }); - - it("should persist on reload", () => { - cy.reload(); - cy.get(".stock-levels").contains("Stock Levels").should("be.visible"); - }); -}); diff --git a/cypress/integration/test_organizational_chart_desktop.js b/cypress/integration/test_organizational_chart_desktop.js deleted file mode 100644 index 464cce48d0..0000000000 --- a/cypress/integration/test_organizational_chart_desktop.js +++ /dev/null @@ -1,116 +0,0 @@ -context('Organizational Chart', () => { - before(() => { - cy.login(); - cy.visit('/app/website'); - }); - - it('navigates to org chart', () => { - cy.visit('/app'); - cy.visit('/app/organizational-chart'); - cy.url().should('include', '/organizational-chart'); - - cy.window().its('frappe.csrf_token').then(csrf_token => { - return cy.request({ - url: `/api/method/erpnext.tests.ui_test_helpers.create_employee_records`, - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'X-Frappe-CSRF-Token': csrf_token - }, - timeout: 60000 - }).then(res => { - expect(res.status).eq(200); - cy.get('.frappe-control[data-fieldname=company] input').focus().as('input'); - cy.get('@input') - .clear({ force: true }) - .type('Test Org Chart{downarrow}{enter}', { force: true }) - .blur({ force: true }); - }); - }); - }); - - it('renders root nodes and loads children for the first expandable node', () => { - // check rendered root nodes and the node name, title, connections - cy.get('.hierarchy').find('.root-level ul.node-children').children() - .should('have.length', 2) - .first() - .as('first-child'); - - cy.get('@first-child').get('.node-name').contains('Test Employee 1'); - cy.get('@first-child').get('.node-info').find('.node-title').contains('CEO'); - cy.get('@first-child').get('.node-info').find('.node-connections').contains('· 2 Connections'); - - cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { - // children of 1st root visible - cy.get(`div[data-parent="${employee_records.message[0]}"]`).as('child-node'); - cy.get('@child-node') - .should('have.length', 1) - .should('be.visible'); - cy.get('@child-node').get('.node-name').contains('Test Employee 3'); - - // connectors between first root node and immediate child - cy.get(`path[data-parent="${employee_records.message[0]}"]`) - .should('be.visible') - .invoke('attr', 'data-child') - .should('equal', employee_records.message[2]); - }); - }); - - it('hides active nodes children and connectors on expanding sibling node', () => { - cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { - // click sibling - cy.get(`#${employee_records.message[1]}`) - .click() - .should('have.class', 'active'); - - // child nodes and connectors hidden - cy.get(`[data-parent="${employee_records.message[0]}"]`).should('not.be.visible'); - cy.get(`path[data-parent="${employee_records.message[0]}"]`).should('not.be.visible'); - }); - }); - - it('collapses previous level nodes and refreshes connectors on expanding child node', () => { - cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { - // click child node - cy.get(`#${employee_records.message[3]}`) - .click() - .should('have.class', 'active'); - - // previous level nodes: parent should be on active-path; other nodes should be collapsed - cy.get(`#${employee_records.message[0]}`).should('have.class', 'collapsed'); - cy.get(`#${employee_records.message[1]}`).should('have.class', 'active-path'); - - // previous level connectors refreshed - cy.get(`path[data-parent="${employee_records.message[1]}"]`) - .should('have.class', 'collapsed-connector'); - - // child node's children and connectors rendered - cy.get(`[data-parent="${employee_records.message[3]}"]`).should('be.visible'); - cy.get(`path[data-parent="${employee_records.message[3]}"]`).should('be.visible'); - }); - }); - - it('expands previous level nodes', () => { - cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { - cy.get(`#${employee_records.message[0]}`) - .click() - .should('have.class', 'active'); - - cy.get(`[data-parent="${employee_records.message[0]}"]`) - .should('be.visible'); - - cy.get('ul.hierarchy').children().should('have.length', 2); - cy.get(`#connectors`).children().should('have.length', 1); - }); - }); - - it('edit node navigates to employee master', () => { - cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { - cy.get(`#${employee_records.message[0]}`).find('.btn-edit-node') - .click(); - - cy.url().should('include', `/employee/${employee_records.message[0]}`); - }); - }); -}); diff --git a/cypress/integration/test_organizational_chart_mobile.js b/cypress/integration/test_organizational_chart_mobile.js deleted file mode 100644 index 971ac6d3ef..0000000000 --- a/cypress/integration/test_organizational_chart_mobile.js +++ /dev/null @@ -1,195 +0,0 @@ -context('Organizational Chart Mobile', () => { - before(() => { - cy.login(); - cy.visit('/app/website'); - }); - - it('navigates to org chart', () => { - cy.viewport(375, 667); - cy.visit('/app'); - cy.visit('/app/organizational-chart'); - cy.url().should('include', '/organizational-chart'); - - cy.window().its('frappe.csrf_token').then(csrf_token => { - return cy.request({ - url: `/api/method/erpnext.tests.ui_test_helpers.create_employee_records`, - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'X-Frappe-CSRF-Token': csrf_token - }, - timeout: 60000 - }).then(res => { - expect(res.status).eq(200); - cy.get('.frappe-control[data-fieldname=company] input').focus().as('input'); - cy.get('@input') - .clear({ force: true }) - .type('Test Org Chart{downarrow}{enter}', { force: true }) - .blur({ force: true }); - }); - }); - }); - - it('renders root nodes', () => { - // check rendered root nodes and the node name, title, connections - cy.get('.hierarchy-mobile').find('.root-level').children() - .should('have.length', 2) - .first() - .as('first-child'); - - cy.get('@first-child').get('.node-name').contains('Test Employee 1'); - cy.get('@first-child').get('.node-info').find('.node-title').contains('CEO'); - cy.get('@first-child').get('.node-info').find('.node-connections').contains('· 2'); - }); - - it('expands root node', () => { - cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { - cy.get(`#${employee_records.message[1]}`) - .click() - .should('have.class', 'active'); - - // other root node removed - cy.get(`#${employee_records.message[0]}`).should('not.exist'); - - // children of active root node - cy.get('.hierarchy-mobile').find('.level').first().find('ul.node-children').children() - .should('have.length', 2); - - cy.get(`div[data-parent="${employee_records.message[1]}"]`).first().as('child-node'); - cy.get('@child-node').should('be.visible'); - - cy.get('@child-node') - .get('.node-name') - .contains('Test Employee 4'); - - // connectors between root node and immediate children - cy.get(`path[data-parent="${employee_records.message[1]}"]`).as('connectors'); - cy.get('@connectors') - .should('have.length', 2) - .should('be.visible'); - - cy.get('@connectors') - .first() - .invoke('attr', 'data-child') - .should('eq', employee_records.message[3]); - }); - }); - - it('expands child node', () => { - cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { - cy.get(`#${employee_records.message[3]}`) - .click() - .should('have.class', 'active') - .as('expanded_node'); - - // 2 levels on screen; 1 on active path; 1 collapsed - cy.get('.hierarchy-mobile').children().should('have.length', 2); - cy.get(`#${employee_records.message[1]}`).should('have.class', 'active-path'); - - // children of expanded node visible - cy.get('@expanded_node') - .next() - .should('have.class', 'node-children') - .as('node-children'); - - cy.get('@node-children').children().should('have.length', 1); - cy.get('@node-children') - .first() - .get('.node-card') - .should('have.class', 'active-child') - .contains('Test Employee 7'); - - // orphan connectors removed - cy.get(`#connectors`).children().should('have.length', 2); - }); - }); - - it('renders sibling group', () => { - cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { - // sibling group visible for parent - cy.get(`#${employee_records.message[1]}`) - .next() - .as('sibling_group'); - - cy.get('@sibling_group') - .should('have.attr', 'data-parent', 'undefined') - .should('have.class', 'node-group') - .and('have.class', 'collapsed'); - - cy.get('@sibling_group').get('.avatar-group').children().as('siblings'); - cy.get('@siblings').should('have.length', 1); - cy.get('@siblings') - .first() - .should('have.attr', 'title', 'Test Employee 1'); - - }); - }); - - it('expands previous level nodes', () => { - cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { - cy.get(`#${employee_records.message[6]}`) - .click() - .should('have.class', 'active'); - - // clicking on previous level node should remove all the nodes ahead - // and expand that node - cy.get(`#${employee_records.message[3]}`).click(); - cy.get(`#${employee_records.message[3]}`) - .should('have.class', 'active') - .should('not.have.class', 'active-path'); - - cy.get(`#${employee_records.message[6]}`).should('have.class', 'active-child'); - cy.get('.hierarchy-mobile').children().should('have.length', 2); - cy.get(`#connectors`).children().should('have.length', 2); - }); - }); - - it('expands sibling group', () => { - cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { - // sibling group visible for parent - cy.get(`#${employee_records.message[6]}`).click(); - - cy.get(`#${employee_records.message[3]}`) - .next() - .click(); - - // siblings of parent should be visible - cy.get('.hierarchy-mobile').prev().as('sibling_group'); - cy.get('@sibling_group') - .should('exist') - .should('have.class', 'sibling-group') - .should('not.have.class', 'collapsed'); - - cy.get(`#${employee_records.message[1]}`) - .should('be.visible') - .should('have.class', 'active'); - - cy.get(`[data-parent="${employee_records.message[1]}"]`) - .should('be.visible') - .should('have.length', 2) - .should('have.class', 'active-child'); - }); - }); - - it('goes to the respective level after clicking on non-collapsed sibling group', () => { - cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(() => { - // click on non-collapsed sibling group - cy.get('.hierarchy-mobile') - .prev() - .click(); - - // should take you to that level - cy.get('.hierarchy-mobile').find('li.level .node-card').should('have.length', 2); - }); - }); - - it('edit node navigates to employee master', () => { - cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => { - cy.get(`#${employee_records.message[0]}`).find('.btn-edit-node') - .click(); - - cy.url().should('include', `/employee/${employee_records.message[0]}`); - }); - }); -}); diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js deleted file mode 100644 index 07d9804a73..0000000000 --- a/cypress/plugins/index.js +++ /dev/null @@ -1,17 +0,0 @@ -// *********************************************************** -// This example plugins/index.js can be used to load plugins -// -// You can change the location of this file or turn off loading -// the plugins file with the 'pluginsFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/plugins-guide -// *********************************************************** - -// This function is called when a project is opened or re-opened (e.g. due to -// the project's config changing) - -module.exports = () => { - // `on` is used to hook into various events Cypress emits - // `config` is the resolved Cypress config -}; diff --git a/cypress/support/commands.js b/cypress/support/commands.js deleted file mode 100644 index 7ddc80ab8d..0000000000 --- a/cypress/support/commands.js +++ /dev/null @@ -1,31 +0,0 @@ -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add("login", (email, password) => { ... }); -// -// -// -- This is a child command -- -// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }); -// -// -// -- This is a dual command -- -// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }); -// -// -// -- This is will overwrite an existing command -- -// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }); - -const slug = (name) => name.toLowerCase().replace(" ", "-"); - -Cypress.Commands.add("go_to_doc", (doctype, name) => { - cy.visit(`/app/${slug(doctype)}/${encodeURIComponent(name)}`); -}); diff --git a/cypress/support/index.js b/cypress/support/index.js deleted file mode 100644 index 72070cc81c..0000000000 --- a/cypress/support/index.js +++ /dev/null @@ -1,26 +0,0 @@ -// *********************************************************** -// This example support/index.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -// Import commands.js using ES2015 syntax: -import './commands'; -import '../../../frappe/cypress/support/commands' // eslint-disable-line - - -// Alternatively you can use CommonJS syntax: -// require('./commands') - -Cypress.Cookies.defaults({ - preserve: 'sid' -}); diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json deleted file mode 100644 index d90ebf6856..0000000000 --- a/cypress/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "allowJs": true, - "baseUrl": "../node_modules", - "types": [ - "cypress" - ] - }, - "include": [ - "**/*.*" - ] -} \ No newline at end of file From c84e11ac82d8fa2dd4d457a4ffd7ea1ca124e482 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 1 Jun 2022 12:55:10 +0530 Subject: [PATCH 128/192] fix: re-validate warehouse after 'update items' (#31203) --- erpnext/controllers/accounts_controller.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 056084b7e8..0dd6a5c333 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2661,7 +2661,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil parent.update_reserved_qty_for_subcontract() parent.create_raw_materials_supplied("supplied_items") parent.save() - else: + else: # Sales Order + parent.validate_warehouse() parent.update_reserved_qty() parent.update_project() parent.update_prevdoc_status("submit") From 536f1dfc4b4c7286bab41ded93c2d221023162d8 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 1 Jun 2022 13:56:55 +0530 Subject: [PATCH 129/192] test: fix attendance tests for unmarked days (#31205) * test: fix attendance tests for unmarked days * chore: remove unused import --- .../hr/doctype/attendance/test_attendance.py | 61 +++++++++++-------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/erpnext/hr/doctype/attendance/test_attendance.py b/erpnext/hr/doctype/attendance/test_attendance.py index 762d0f7567..c85ec6551a 100644 --- a/erpnext/hr/doctype/attendance/test_attendance.py +++ b/erpnext/hr/doctype/attendance/test_attendance.py @@ -3,7 +3,15 @@ import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import add_days, get_year_ending, get_year_start, getdate, now_datetime, nowdate +from frappe.utils import ( + add_days, + add_months, + get_last_day, + get_year_ending, + get_year_start, + getdate, + nowdate, +) from erpnext.hr.doctype.attendance.attendance import ( DuplicateAttendanceError, @@ -138,69 +146,70 @@ class TestAttendance(FrappeTestCase): self.assertEqual(attendance, fetch_attendance) def test_unmarked_days(self): - now = now_datetime() - previous_month = now.month - 1 - first_day = now.replace(day=1).replace(month=previous_month).date() + first_sunday = get_first_sunday( + self.holiday_list, for_date=get_last_day(add_months(getdate(), -1)) + ) + attendance_date = add_days(first_sunday, 1) employee = make_employee( - "test_unmarked_days@example.com", date_of_joining=add_days(first_day, -1) + "test_unmarked_days@example.com", date_of_joining=add_days(attendance_date, -1) ) frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list) - first_sunday = get_first_sunday(self.holiday_list, for_date=first_day) - mark_attendance(employee, first_day, "Present") - month_name = get_month_name(first_day) + mark_attendance(employee, attendance_date, "Present") + month_name = get_month_name(attendance_date) unmarked_days = get_unmarked_days(employee, month_name) unmarked_days = [getdate(date) for date in unmarked_days] # attendance already marked for the day - self.assertNotIn(first_day, unmarked_days) + self.assertNotIn(attendance_date, unmarked_days) # attendance unmarked - self.assertIn(getdate(add_days(first_day, 1)), unmarked_days) + self.assertIn(getdate(add_days(attendance_date, 1)), unmarked_days) # holiday considered in unmarked days self.assertIn(first_sunday, unmarked_days) def test_unmarked_days_excluding_holidays(self): - now = now_datetime() - previous_month = now.month - 1 - first_day = now.replace(day=1).replace(month=previous_month).date() + first_sunday = get_first_sunday( + self.holiday_list, for_date=get_last_day(add_months(getdate(), -1)) + ) + attendance_date = add_days(first_sunday, 1) employee = make_employee( - "test_unmarked_days@example.com", date_of_joining=add_days(first_day, -1) + "test_unmarked_days@example.com", date_of_joining=add_days(attendance_date, -1) ) frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list) - first_sunday = get_first_sunday(self.holiday_list, for_date=first_day) - mark_attendance(employee, first_day, "Present") - month_name = get_month_name(first_day) + mark_attendance(employee, attendance_date, "Present") + month_name = get_month_name(attendance_date) unmarked_days = get_unmarked_days(employee, month_name, exclude_holidays=True) unmarked_days = [getdate(date) for date in unmarked_days] # attendance already marked for the day - self.assertNotIn(first_day, unmarked_days) + self.assertNotIn(attendance_date, unmarked_days) # attendance unmarked - self.assertIn(getdate(add_days(first_day, 1)), unmarked_days) + self.assertIn(getdate(add_days(attendance_date, 1)), unmarked_days) # holidays not considered in unmarked days self.assertNotIn(first_sunday, unmarked_days) def test_unmarked_days_as_per_joining_and_relieving_dates(self): - now = now_datetime() - previous_month = now.month - 1 - first_day = now.replace(day=1).replace(month=previous_month).date() + first_sunday = get_first_sunday( + self.holiday_list, for_date=get_last_day(add_months(getdate(), -1)) + ) + date = add_days(first_sunday, 1) - doj = add_days(first_day, 1) - relieving_date = add_days(first_day, 5) + doj = add_days(date, 1) + relieving_date = add_days(date, 5) employee = make_employee( "test_unmarked_days_as_per_doj@example.com", date_of_joining=doj, relieving_date=relieving_date ) frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list) - attendance_date = add_days(first_day, 2) + attendance_date = add_days(date, 2) mark_attendance(employee, attendance_date, "Present") - month_name = get_month_name(first_day) + month_name = get_month_name(attendance_date) unmarked_days = get_unmarked_days(employee, month_name) unmarked_days = [getdate(date) for date in unmarked_days] From 37433aad48ce9a6b22fb3b20accef95eaea2c540 Mon Sep 17 00:00:00 2001 From: Mohammad Hussain Nagaria <34810212+NagariaHussain@users.noreply.github.com> Date: Wed, 1 Jun 2022 16:29:20 +0530 Subject: [PATCH 130/192] fix: Pluralize year text instead of optional bracket (#31210) Co-authored-by: Rucha Mahabal --- erpnext/hr/doctype/employee/employee_reminders.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/employee/employee_reminders.py b/erpnext/hr/doctype/employee/employee_reminders.py index 1829bc4f2f..f09d7ff75a 100644 --- a/erpnext/hr/doctype/employee/employee_reminders.py +++ b/erpnext/hr/doctype/employee/employee_reminders.py @@ -230,7 +230,7 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons): persons_name = anniversary_person # Number of years completed at the company completed_years = getdate().year - anniversary_persons[0]["date_of_joining"].year - anniversary_person += f" completed {completed_years} year(s)" + anniversary_person += f" completed {get_pluralized_years(completed_years)}" else: person_names_with_years = [] names = [] @@ -239,7 +239,7 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons): names.append(person_text) # Number of years completed at the company completed_years = getdate().year - person["date_of_joining"].year - person_text += f" completed {completed_years} year(s)" + person_text += f" completed {get_pluralized_years(completed_years)}" person_names_with_years.append(person_text) # converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim @@ -254,6 +254,12 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons): return reminder_text, message +def get_pluralized_years(years): + if years == 1: + return "1 year" + return f"{years} years" + + def send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message): frappe.sendmail( recipients=recipients, From 3974fbbb6e0f4f41b292b870b80d6eab300f5e32 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 1 Jun 2022 16:43:56 +0530 Subject: [PATCH 131/192] feat: UOM specific barcodes (#30988) --- erpnext/public/js/controllers/transaction.js | 3 ++- erpnext/public/js/utils/barcode_scanner.js | 23 ++++++++++++++----- .../doctype/item_barcode/item_barcode.json | 12 ++++++++-- erpnext/stock/utils.py | 2 +- 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index edc4b06dca..de93c82ef2 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -423,7 +423,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe item.barcode = null; - if(item.item_code || item.barcode || item.serial_no) { + if(item.item_code || item.serial_no) { if(!this.validate_company_and_party()) { this.frm.fields_dict["items"].grid.grid_rows[item.idx - 1].remove(); } else { @@ -463,6 +463,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe stock_qty: item.stock_qty, conversion_factor: item.conversion_factor, weight_per_unit: item.weight_per_unit, + uom: item.uom, weight_uom: item.weight_uom, manufacturer: item.manufacturer, stock_uom: item.stock_uom, diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js index 943db07705..a6bff2c148 100644 --- a/erpnext/public/js/utils/barcode_scanner.js +++ b/erpnext/public/js/utils/barcode_scanner.js @@ -9,6 +9,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { this.barcode_field = opts.barcode_field || "barcode"; this.serial_no_field = opts.serial_no_field || "serial_no"; this.batch_no_field = opts.batch_no_field || "batch_no"; + this.uom_field = opts.uom_field || "uom"; this.qty_field = opts.qty_field || "qty"; // field name on row which defines max quantity to be scanned e.g. picklist this.max_qty_field = opts.max_qty_field; @@ -26,6 +27,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { // bar_code: "123456", // present if barcode was scanned // batch_no: "LOT12", // present if batch was scanned // serial_no: "987XYZ", // present if serial no was scanned + // uom: "Kg", // present if barcode UOM is different from default // } this.scan_api = opts.scan_api || "erpnext.stock.utils.scan_barcode"; } @@ -67,9 +69,9 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { return new Promise(resolve => { let cur_grid = this.frm.fields_dict[this.items_table_name].grid; - const {item_code, barcode, batch_no, serial_no} = data; + const {item_code, barcode, batch_no, serial_no, uom} = data; - let row = this.get_row_to_modify_on_scan(item_code, batch_no); + let row = this.get_row_to_modify_on_scan(item_code, batch_no, uom); if (!row) { if (this.dont_allow_new_row) { @@ -90,10 +92,11 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { } frappe.run_serially([ - () => this.set_selector_trigger_flag(row, data), + () => this.set_selector_trigger_flag(data), () => this.set_item(row, item_code).then(qty => { this.show_scan_message(row.idx, row.item_code, qty); }), + () => this.set_barcode_uom(row, uom), () => this.set_serial_no(row, serial_no), () => this.set_batch_no(row, batch_no), () => this.set_barcode(row, barcode), @@ -106,7 +109,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { // batch and serial selector is reduandant when all info can be added by scan // this flag on item row is used by transaction.js to avoid triggering selector - set_selector_trigger_flag(row, data) { + set_selector_trigger_flag(data) { const {batch_no, serial_no, has_batch_no, has_serial_no} = data; const require_selecting_batch = has_batch_no && !batch_no; @@ -154,6 +157,12 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { } } + async set_barcode_uom(row, uom) { + if (uom && frappe.meta.has_field(row.doctype, this.uom_field)) { + await frappe.model.set_value(row.doctype, row.name, this.uom_field, uom); + } + } + async set_batch_no(row, batch_no) { if (batch_no && frappe.meta.has_field(row.doctype, this.batch_no_field)) { await frappe.model.set_value(row.doctype, row.name, this.batch_no_field, batch_no); @@ -184,7 +193,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { return is_duplicate; } - get_row_to_modify_on_scan(item_code, batch_no) { + get_row_to_modify_on_scan(item_code, batch_no, uom) { let cur_grid = this.frm.fields_dict[this.items_table_name].grid; // Check if batch is scanned and table has batch no field @@ -193,10 +202,12 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { const matching_row = (row) => { const item_match = row.item_code == item_code; - const batch_match = row.batch_no == batch_no; + const batch_match = row[this.batch_no_field] == batch_no; + const uom_match = !uom || row[this.uom_field] == uom; const qty_in_limit = flt(row[this.qty_field]) < flt(row[this.max_qty_field]); return item_match + && uom_match && (!is_batch_no_scan || batch_match) && (!check_max_qty || qty_in_limit) } diff --git a/erpnext/stock/doctype/item_barcode/item_barcode.json b/erpnext/stock/doctype/item_barcode/item_barcode.json index eef70c95d0..56832f32d3 100644 --- a/erpnext/stock/doctype/item_barcode/item_barcode.json +++ b/erpnext/stock/doctype/item_barcode/item_barcode.json @@ -6,7 +6,8 @@ "engine": "InnoDB", "field_order": [ "barcode", - "barcode_type" + "barcode_type", + "uom" ], "fields": [ { @@ -24,11 +25,18 @@ "in_list_view": 1, "label": "Barcode Type", "options": "\nEAN\nUPC-A" + }, + { + "fieldname": "uom", + "fieldtype": "Link", + "in_list_view": 1, + "label": "UOM", + "options": "UOM" } ], "istable": 1, "links": [], - "modified": "2022-04-01 05:54:27.314030", + "modified": "2022-06-01 06:24:33.969534", "modified_by": "Administrator", "module": "Stock", "name": "Item Barcode", diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 2120304097..6d8fdaa404 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -558,7 +558,7 @@ def scan_barcode(search_value: str) -> Dict[str, Optional[str]]: barcode_data = frappe.db.get_value( "Item Barcode", {"barcode": search_value}, - ["barcode", "parent as item_code"], + ["barcode", "parent as item_code", "uom"], as_dict=True, ) if barcode_data: From 661e05e6937fe75d9acc331dc4b18be5b16ec638 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 1 Jun 2022 17:28:42 +0530 Subject: [PATCH 132/192] fix(tests): account and company setups --- .../payroll_entry/test_payroll_entry.py | 55 ++++++------------- 1 file changed, 18 insertions(+), 37 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index 47b9962912..84f1575381 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -86,44 +86,32 @@ class TestPayrollEntry(FrappeTestCase): ) def test_multi_currency_payroll_entry(self): - company = erpnext.get_default_company() - employee = make_employee("test_muti_currency_employee@payroll.com", company=company) - for data in frappe.get_all("Salary Component", fields=["name"]): - if not frappe.db.get_value( - "Salary Component Account", {"parent": data.name, "company": company}, "name" - ): - get_salary_component_account(data.name) + company = frappe.get_doc("Company", "_Test Company") + employee = make_employee( + "test_muti_currency_employee@payroll.com", company=company.name, department="Accounts - _TC" + ) + salary_structure = "_Test Multi Currency Salary Structure" + setup_salary_structure(employee, company, "USD", salary_structure) - company_doc = frappe.get_doc("Company", company) - salary_structure = make_salary_structure( - "_Test Multi Currency Salary Structure", "Monthly", company=company, currency="USD" - ) - create_salary_structure_assignment( - employee, salary_structure.name, company=company, currency="USD" - ) - frappe.db.sql( - """delete from `tabSalary Slip` where employee=%s""", - (frappe.db.get_value("Employee", {"user_id": "test_muti_currency_employee@payroll.com"})), - ) - salary_slip = get_salary_slip( - "test_muti_currency_employee@payroll.com", "Monthly", "_Test Multi Currency Salary Structure" - ) dates = get_start_end_dates("Monthly", nowdate()) payroll_entry = make_payroll_entry( start_date=dates.start_date, end_date=dates.end_date, - payable_account=company_doc.default_payroll_payable_account, + payable_account=company.default_payroll_payable_account, currency="USD", exchange_rate=70, + company=company.name, + cost_center="Main - _TC", ) payroll_entry.make_payment_entry() - salary_slip.load_from_db() + salary_slip = frappe.db.get_value("Salary Slip", {"payroll_entry": payroll_entry.name}, "name") + salary_slip = frappe.get_doc("Salary Slip", salary_slip) + payroll_entry.reload() payroll_je = salary_slip.journal_entry if payroll_je: payroll_je_doc = frappe.get_doc("Journal Entry", payroll_je) - self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_debit) self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_credit) @@ -136,7 +124,6 @@ class TestPayrollEntry(FrappeTestCase): (payroll_entry.name), as_dict=1, ) - self.assertEqual(salary_slip.base_net_pay, payment_entry[0].total_debit) self.assertEqual(salary_slip.base_net_pay, payment_entry[0].total_credit) @@ -306,7 +293,7 @@ class TestPayrollEntry(FrappeTestCase): def test_salary_slip_operation_queueing(self): company = "_Test Company" company_doc = frappe.get_doc("Company", company) - employee = frappe.db.get_value("Employee", {"company": company}) + employee = make_employee("test_employee@payroll.com", company=company) setup_salary_structure(employee, company_doc) # enqueue salary slip creation via payroll entry @@ -318,6 +305,7 @@ class TestPayrollEntry(FrappeTestCase): payable_account=company_doc.default_payroll_payable_account, currency=company_doc.default_currency, company=company_doc.name, + cost_center="Main - _TC", ) frappe.flags.enqueue_payroll_entry = True payroll_entry.create_salary_slips() @@ -329,7 +317,7 @@ class TestPayrollEntry(FrappeTestCase): def test_salary_slip_operation_failure(self): company = "_Test Company" company_doc = frappe.get_doc("Company", company) - employee = frappe.db.get_value("Employee", {"company": company}) + employee = make_employee("test_employee@payroll.com", company=company) salary_structure = make_salary_structure( "_Test Salary Structure", @@ -353,6 +341,7 @@ class TestPayrollEntry(FrappeTestCase): payable_account=company_doc.default_payroll_payable_account, currency=company_doc.default_currency, company=company_doc.name, + cost_center="Main - _TC", ) payroll_entry.create_salary_slips() payroll_entry.submit_salary_slips() @@ -375,7 +364,7 @@ class TestPayrollEntry(FrappeTestCase): def test_payroll_entry_status(self): company = "_Test Company" company_doc = frappe.get_doc("Company", company) - employee = frappe.db.get_value("Employee", {"company": company}) + employee = make_employee("test_employee@payroll.com", company=company) setup_salary_structure(employee, company_doc) @@ -386,6 +375,7 @@ class TestPayrollEntry(FrappeTestCase): payable_account=company_doc.default_payroll_payable_account, currency=company_doc.default_currency, company=company_doc.name, + cost_center="Main - _TC", ) payroll_entry.submit() self.assertEqual(payroll_entry.status, "Submitted") @@ -466,15 +456,6 @@ def make_holiday(holiday_list_name): return holiday_list_name -def get_salary_slip(user, period, salary_structure): - salary_slip = make_employee_salary_slip(user, period, salary_structure) - salary_slip.exchange_rate = 70 - salary_slip.calculate_net_pay() - salary_slip.db_update() - - return salary_slip - - def setup_salary_structure(employee, company_doc, currency=None, salary_structure=None): for data in frappe.get_all("Salary Component", pluck="name"): if not frappe.db.get_value( From 48bde2de2a2280d788f7688f9cb523d76042ffcf Mon Sep 17 00:00:00 2001 From: Sun Howwrongbum Date: Wed, 1 Jun 2022 20:20:16 +0530 Subject: [PATCH 133/192] fix: Trial Balance failing to ignore Finance Book --- .../accounts/report/trial_balance/trial_balance.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index e5a4ed2f34..af447df52a 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -160,14 +160,10 @@ def get_rootwise_opening_balances(filters, report_type): if filters.project: additional_conditions += " and project = %(project)s" - if filters.finance_book: - fb_conditions = " AND finance_book = %(finance_book)s" - if filters.include_default_book_entries: - fb_conditions = ( - " AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)" - ) - - additional_conditions += fb_conditions + if filters.get("include_default_book_entries"): + additional_conditions += "AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)" + else: + additional_conditions += "AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)" accounting_dimensions = get_accounting_dimensions(as_list=False) From 77dcdff0db39f3dfe06a41733039b52bbf8c4caa Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 1 Jun 2022 22:01:07 +0530 Subject: [PATCH 134/192] fix: unusable SO after clearing taxes (#31215) --- erpnext/controllers/accounts_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 0dd6a5c333..bebfa6c76f 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1866,7 +1866,7 @@ def get_default_taxes_and_charges(master_doctype, tax_template=None, company=Non def get_taxes_and_charges(master_doctype, master_name): if not master_name: return - from frappe.model import default_fields + from frappe.model import child_table_fields, default_fields tax_master = frappe.get_doc(master_doctype, master_name) @@ -1874,7 +1874,7 @@ def get_taxes_and_charges(master_doctype, master_name): for i, tax in enumerate(tax_master.get("taxes")): tax = tax.as_dict() - for fieldname in default_fields: + for fieldname in default_fields + child_table_fields: if fieldname in tax: del tax[fieldname] From c7efa3b44d033c5214fbf6453954b7c5de25e037 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 2 Jun 2022 12:27:11 +0530 Subject: [PATCH 135/192] ci: stale apt cache (#31217) --- .github/helper/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/helper/install.sh b/.github/helper/install.sh index 69749c93af..f0f83b061b 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -11,7 +11,7 @@ fi cd ~ || exit -sudo apt-get install redis-server libcups2-dev +sudo apt update && sudo apt install redis-server libcups2-dev pip install frappe-bench From f0ac394d6e5d284a476eba62a6fcd6aaa01dd00d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 2 Jun 2022 12:59:55 +0530 Subject: [PATCH 136/192] fix(India): GSTIN filter in GSTR-1 report --- erpnext/regional/report/gstr_1/gstr_1.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 0bdbe56de6..6cbc12c7a1 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -1155,8 +1155,11 @@ def get_company_gstins(company): .inner_join(links) .on(address.name == links.parent) .select(address.gstin) + .distinct() .where(links.link_doctype == "Company") .where(links.link_name == company) + .where(address.gstin.isnotnull()) + .where(address.gstin != "") .run(as_dict=1) ) From 62857e3e080b3888f40a09112be63238974dd175 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 2 Jun 2022 13:35:30 +0530 Subject: [PATCH 137/192] feat: Track progress in Log Batch/Job wise - This was done due to stale reads while the background jobs tried updating status of the log - Added a table where all bom jobs within log will be tracked with what level they are processing - Cron job will check if table jobs are all processed every 5 mins - If yes, it will prepare parents and call `process_boms_cost_level_wise` to start next level - If pending jobs, do nothing - Current BOM Level is being tracked that helps adding rows to the table - Individual bom cost jobs (that are queued) will process and update boms > will update BOM Update Batch table row with list of updated BOMs --- .../doctype/bom_update_batch/__init__.py | 0 .../bom_update_batch/bom_update_batch.json | 45 ++++++++ .../bom_update_batch/bom_update_batch.py | 9 ++ .../bom_update_log/bom_update_log.json | 21 ++-- .../doctype/bom_update_log/bom_update_log.py | 106 +++++++++++++----- .../bom_update_log/bom_updation_utils.py | 55 +-------- 6 files changed, 154 insertions(+), 82 deletions(-) create mode 100644 erpnext/manufacturing/doctype/bom_update_batch/__init__.py create mode 100644 erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json create mode 100644 erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.py diff --git a/erpnext/manufacturing/doctype/bom_update_batch/__init__.py b/erpnext/manufacturing/doctype/bom_update_batch/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json b/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json new file mode 100644 index 0000000000..9938454ce4 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json @@ -0,0 +1,45 @@ +{ + "actions": [], + "autoname": "autoincrement", + "creation": "2022-05-31 17:34:39.825537", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "level", + "batch_no", + "boms_updated" + ], + "fields": [ + { + "fieldname": "level", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Level" + }, + { + "fieldname": "batch_no", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Batch No." + }, + { + "fieldname": "boms_updated", + "fieldtype": "Long Text", + "in_list_view": 1, + "label": "BOMs Updated" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2022-05-31 23:36:13.628391", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "BOM Update Batch", + "naming_rule": "Autoincrement", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.py b/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.py new file mode 100644 index 0000000000..f952e435e6 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class BOMUpdateBatch(Document): + pass diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json index bea3cf0373..b1c24ab995 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json @@ -14,9 +14,10 @@ "status", "error_log", "progress_section", - "current_boms", + "current_level", "parent_boms", "processed_boms", + "bom_batches", "amended_from" ], "fields": [ @@ -70,15 +71,11 @@ }, { "collapsible": 1, + "depends_on": "eval: doc.update_type == \"Update Cost\"", "fieldname": "progress_section", "fieldtype": "Section Break", "label": "Progress" }, - { - "fieldname": "current_boms", - "fieldtype": "Long Text", - "label": "Current BOMs" - }, { "description": "Immediate parent BOMs", "fieldname": "parent_boms", @@ -89,13 +86,23 @@ "fieldname": "processed_boms", "fieldtype": "Long Text", "label": "Processed BOMs" + }, + { + "fieldname": "bom_batches", + "fieldtype": "Table", + "options": "BOM Update Batch" + }, + { + "fieldname": "current_level", + "fieldtype": "Int", + "label": "Current Level" } ], "in_create": 1, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-05-27 17:03:34.712010", + "modified": "2022-05-31 20:20:06.370786", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Update Log", diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index f61f863c10..bfae76c2b2 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -1,15 +1,16 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt import json -from typing import Dict, Optional +from typing import Any, Dict, List, Optional, Tuple import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cstr +from frappe.utils import cint, cstr from erpnext.manufacturing.doctype.bom_update_log.bom_updation_utils import ( get_leaf_boms, + get_next_higher_level_boms, handle_exception, replace_bom, set_values_in_log, @@ -111,55 +112,110 @@ def process_boms_cost_level_wise(update_doc: "BOMUpdateLog") -> None: if update_doc.status == "Queued": # First level yet to process. On Submit. - current_boms = {bom: False for bom in get_leaf_boms()} + current_level = 0 + current_boms = get_leaf_boms() values = { - "current_boms": json.dumps(current_boms), "parent_boms": "[]", "processed_boms": json.dumps({}), "status": "In Progress", + "current_level": current_level, } else: - # status is Paused, resume. via Cron Job. - current_boms, parent_boms = json.loads(update_doc.current_boms), json.loads( - update_doc.parent_boms - ) - if not current_boms: - # Process the next level BOMs. Stage parents as current BOMs. - current_boms = {bom: False for bom in parent_boms} - values = { - "current_boms": json.dumps(current_boms), - "parent_boms": "[]", - "status": "In Progress", - } + # Resume next level. via Cron Job. + current_level = cint(update_doc.current_level) + 1 + parent_boms = json.loads(update_doc.parent_boms) + + # Process the next level BOMs. Stage parents as current BOMs. + current_boms = parent_boms.copy() + values = {"parent_boms": "[]", "current_level": current_level} set_values_in_log(update_doc.name, values, commit=True) - queue_bom_cost_jobs(current_boms, update_doc) + queue_bom_cost_jobs(current_boms, update_doc, current_level) -def queue_bom_cost_jobs(current_boms: Dict, update_doc: "BOMUpdateLog") -> None: +def queue_bom_cost_jobs( + current_boms_list: List, update_doc: "BOMUpdateLog", current_level: int +) -> None: "Queue batches of 20k BOMs of the same level to process parallelly" - current_boms_list = [bom for bom in current_boms] + batch_no = 0 while current_boms_list: + batch_no += 1 batch_size = 20_000 boms_to_process = current_boms_list[:batch_size] # slice out batch of 20k BOMs # update list to exclude 20K (queued) BOMs current_boms_list = current_boms_list[batch_size:] if len(current_boms_list) > batch_size else [] + + batch_row = update_doc.append("bom_batches", {"level": current_level, "batch_no": batch_no}) + batch_row.db_insert() + frappe.enqueue( method="erpnext.manufacturing.doctype.bom_update_log.bom_updation_utils.update_cost_in_level", doc=update_doc, bom_list=boms_to_process, + batch_name=batch_row.name, timeout=40000, ) def resume_bom_cost_update_jobs(): - "Called every 10 minutes via Cron job." - paused_jobs = frappe.db.get_all("BOM Update Log", {"status": "Paused"}) - if not paused_jobs: + """ + 1. Checks for In Progress BOM Update Log. + 2. Checks if this job has completed the _current level_. + 3. If current level is complete, get parent BOMs and start next level. + 4. If no parents, mark as Complete. + 5. If current level is WIP, skip the Log. + + Called every 5 minutes via Cron job. + """ + + in_progress_logs = frappe.db.get_all( + "BOM Update Log", + {"update_type": "Update Cost", "status": "In Progress"}, + ["name", "processed_boms", "current_level"], + ) + if not in_progress_logs: return - for job in paused_jobs: - # resume from next level - process_boms_cost_level_wise(update_doc=frappe.get_doc("BOM Update Log", job.name)) + for log in in_progress_logs: + # check if all log batches of current level are processed + bom_batches = frappe.db.get_all( + "BOM Update Batch", {"parent": log.name, "level": log.current_level}, ["name", "boms_updated"] + ) + incomplete_level = any(not row.get("boms_updated") for row in bom_batches) + if not bom_batches or incomplete_level: + continue + + # Prep parent BOMs & updated processed BOMs for next level + current_boms, processed_boms = get_processed_current_boms(log, bom_batches) + parent_boms = get_next_higher_level_boms(child_boms=current_boms, processed_boms=processed_boms) + + set_values_in_log( + log.name, + values={ + "processed_boms": json.dumps(processed_boms), + "parent_boms": json.dumps(parent_boms), + "status": "Completed" if not parent_boms else "In Progress", + }, + commit=True, + ) + + if parent_boms: # there is a next level to process + process_boms_cost_level_wise(update_doc=frappe.get_doc("BOM Update Log", log.name)) + + +def get_processed_current_boms( + log: Dict[str, Any], bom_batches: Dict[str, Any] +) -> Tuple[List[str], Dict[str, Any]]: + "Aggregate all BOMs from BOM Update Batch rows into 'processed_boms' field and into current boms list." + processed_boms = json.loads(log.processed_boms) if log.processed_boms else {} + current_boms = [] + + for row in bom_batches: + boms_updated = json.loads(row.boms_updated) + current_boms.extend(boms_updated) + boms_updated_dict = {bom: True for bom in boms_updated} + processed_boms.update(boms_updated_dict) + + return current_boms, processed_boms diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py index 790a79b333..2d6429b050 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py @@ -38,7 +38,7 @@ def replace_bom(boms: Dict) -> None: bom_obj.save_version() -def update_cost_in_level(doc: "BOMUpdateLog", bom_list: List[str]) -> None: +def update_cost_in_level(doc: "BOMUpdateLog", bom_list: List[str], batch_name: int) -> None: "Updates Cost for BOMs within a given level. Runs via background jobs." try: @@ -47,19 +47,9 @@ def update_cost_in_level(doc: "BOMUpdateLog", bom_list: List[str]) -> None: return frappe.db.auto_commit_on_many_writes = 1 - # main updation logic - job_data = update_cost_in_boms(bom_list=bom_list, docname=doc.name) - set_values_in_log( - doc.name, - values={ - "current_boms": json.dumps(job_data.get("current_boms")), - "processed_boms": json.dumps(job_data.get("processed_boms")), - }, - commit=True, - ) - - process_if_level_is_complete(doc.name, job_data["current_boms"], job_data["processed_boms"]) + update_cost_in_boms(bom_list=bom_list) # main updation logic + frappe.db.set_value("BOM Update Batch", batch_name, "boms_updated", json.dumps(bom_list)) except Exception: handle_exception(doc) finally: @@ -112,48 +102,13 @@ def get_bom_unit_cost(bom_name: str) -> float: return frappe.utils.flt(new_bom_unitcost[0][0]) -def update_cost_in_boms(bom_list: List[str], docname: str) -> Dict[str, Dict]: +def update_cost_in_boms(bom_list: List[str]) -> None: "Updates cost in given BOMs. Returns current and total updated BOMs." - updated_boms = {} # current boms that have been updated - for bom in bom_list: bom_doc = frappe.get_cached_doc("BOM", bom) bom_doc.calculate_cost(save_updates=True, update_hour_rate=True) bom_doc.db_update() - updated_boms[bom] = True - - # Update processed BOMs in Log - log_data = frappe.db.get_values( - "BOM Update Log", docname, ["current_boms", "processed_boms"], as_dict=True - )[0] - - for field in ("current_boms", "processed_boms"): - log_data[field] = json.loads(log_data.get(field)) - log_data[field].update(updated_boms) - - return log_data - - -def process_if_level_is_complete( - docname: str, current_boms: Dict[str, bool], processed_boms: Dict[str, bool] -) -> None: - "Prepare and set higher level BOMs/dependants in Log if current level is complete." - - processing_complete = all(current_boms.get(bom) for bom in current_boms) - if not processing_complete: - return - - parent_boms = get_next_higher_level_boms(child_boms=current_boms, processed_boms=processed_boms) - set_values_in_log( - docname, - values={ - "current_boms": json.dumps({}), - "parent_boms": json.dumps(parent_boms), - "status": "Completed" if not parent_boms else "Paused", - }, - commit=True, - ) def get_next_higher_level_boms( @@ -244,7 +199,7 @@ def set_values_in_log(log_name: str, values: Dict[str, Any], commit: bool = Fals query.run() if commit: - frappe.db.commit() + frappe.db.commit() # nosemgrep def handle_exception(doc: "BOMUpdateLog") -> None: From 3f376cc3a5b7f6f28957e032976d31287f7f88cb Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 2 Jun 2022 13:57:54 +0530 Subject: [PATCH 138/192] fix: Parent dimension filters in orders --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js | 2 -- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 1 - erpnext/buying/doctype/purchase_order/purchase_order.js | 2 -- erpnext/public/js/controllers/buying.js | 1 + erpnext/selling/sales_common.js | 1 + erpnext/stock/doctype/delivery_note/delivery_note.js | 2 -- erpnext/stock/doctype/purchase_receipt/purchase_receipt.js | 2 -- 7 files changed, 2 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 42917f811d..7e3597e491 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -45,8 +45,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. if (this.frm.doc.supplier && this.frm.doc.__islocal) { this.frm.trigger('supplier'); } - - erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype); } refresh(doc) { diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 9dde85fe12..aefa9a59dd 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -52,7 +52,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e me.frm.refresh_fields(); } erpnext.queries.setup_warehouse_query(this.frm); - erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype); } refresh(doc, dt, dn) { diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index c9e67987c6..da45610eaf 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -43,8 +43,6 @@ frappe.ui.form.on("Purchase Order", { erpnext.queries.setup_queries(frm, "Warehouse", function() { return erpnext.queries.warehouse(frm.doc); }); - - erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, apply_tds: function(frm) { diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 58eb891600..a5b7699040 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -74,6 +74,7 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac me.frm.set_query('supplier_address', erpnext.queries.address_query); me.frm.set_query('billing_address', erpnext.queries.company_address_query); + erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype); if(this.frm.fields_dict.supplier) { this.frm.set_query("supplier", function() { diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 0954de4e66..6cb53c3bbe 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -43,6 +43,7 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran me.frm.set_query('shipping_address_name', erpnext.queries.address_query); me.frm.set_query('dispatch_address_name', erpnext.queries.dispatch_address_query); + erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype); if(this.frm.fields_dict.selling_price_list) { this.frm.set_query("selling_price_list", function() { diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 706ca36598..ea3cf1948b 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -77,8 +77,6 @@ frappe.ui.form.on("Delivery Note", { } }); - erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); - frm.set_df_property('packed_items', 'cannot_add_rows', true); frm.set_df_property('packed_items', 'cannot_delete_rows', true); }, diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 51ec598f72..754404b068 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -46,8 +46,6 @@ frappe.ui.form.on("Purchase Receipt", { erpnext.queries.setup_queries(frm, "Warehouse", function() { return erpnext.queries.warehouse(frm.doc); }); - - erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, refresh: function(frm) { From d641f260352d5aff7ec77def19bb318dca4a5054 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 2 Jun 2022 15:08:49 +0530 Subject: [PATCH 139/192] fix: error handling and messages - remove savepoints since submission should stop if any error occurs - refactor variable naming and msgprints - test Salary Slip creation failure - fix(test): explicitly commit after payroll entry creation so that the first salary slip creation failure does not rollback the Payroll Entry insert --- .../doctype/payroll_entry/payroll_entry.py | 38 +++++++++---------- .../payroll_entry/test_payroll_entry.py | 28 ++++++++++---- 2 files changed, 38 insertions(+), 28 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index edcada1451..620fcadceb 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -54,7 +54,7 @@ class PayrollEntry(Document): if self.validate_employee_attendance(): frappe.throw(_("Cannot Submit, Employees left to mark attendance")) - def set_status(self, status=None, update=True): + def set_status(self, status=None, update=False): if not status: status = {0: "Draft", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0] @@ -850,7 +850,6 @@ def log_payroll_failure(process, payroll_entry, error): def create_salary_slips_for_employees(employees, args, publish_progress=True): try: - frappe.db.savepoint("salary_slip_creation") payroll_entry = frappe.get_doc("Payroll Entry", args.payroll_entry) salary_slips_exist_for = get_existing_salary_slips(employees, args) count = 0 @@ -879,7 +878,7 @@ def create_salary_slips_for_employees(employees, args, publish_progress=True): ) except Exception as e: - frappe.db.rollback(save_point="salary_slip_creation") + frappe.db.rollback() log_payroll_failure("creation", payroll_entry, e) finally: @@ -887,24 +886,25 @@ def create_salary_slips_for_employees(employees, args, publish_progress=True): frappe.publish_realtime("completed_salary_slip_creation") -def show_payroll_submission_status(submitted, not_submitted, salary_slip): - if not submitted and not not_submitted: +def show_payroll_submission_status(submitted, unsubmitted, payroll_entry): + if not submitted and not unsubmitted: frappe.msgprint( _( "No salary slip found to submit for the above selected criteria OR salary slip already submitted" ) ) - return - - if submitted: + elif submitted and not unsubmitted: frappe.msgprint( - _("Salary Slip submitted for period from {0} to {1}").format( - salary_slip.start_date, salary_slip.end_date + _("Salary Slips submitted for period from {0} to {1}").format( + payroll_entry.start_date, payroll_entry.end_date + ) + ) + elif unsubmitted: + frappe.msgprint( + _("Could not submit some Salary Slips: {}").format( + ", ".join(get_link_to_form("Salary Slip", entry) for entry in unsubmitted) ) ) - - if not_submitted: - frappe.msgprint(_("Could not submit some Salary Slips")) def get_existing_salary_slips(employees, args): @@ -922,23 +922,21 @@ def get_existing_salary_slips(employees, args): def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progress=True): try: - frappe.db.savepoint("salary_slip_submission") - submitted = [] - not_submitted = [] + unsubmitted = [] frappe.flags.via_payroll_entry = True count = 0 for entry in salary_slips: salary_slip = frappe.get_doc("Salary Slip", entry[0]) if salary_slip.net_pay < 0: - not_submitted.append(entry[0]) + unsubmitted.append(entry[0]) else: try: salary_slip.submit() submitted.append(salary_slip) except frappe.ValidationError: - not_submitted.append(entry[0]) + unsubmitted.append(entry[0]) count += 1 if publish_progress: @@ -949,10 +947,10 @@ def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progr payroll_entry.email_salary_slip(submitted) payroll_entry.db_set({"salary_slips_submitted": 1, "status": "Submitted", "error_message": ""}) - show_payroll_submission_status(submitted, not_submitted, salary_slip) + show_payroll_submission_status(submitted, unsubmitted, payroll_entry) except Exception as e: - frappe.db.rollback(save_point="salary_slip_submission") + frappe.db.rollback() log_payroll_failure("submission", payroll_entry, e) finally: diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index 84f1575381..0363a0c3de 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -299,7 +299,7 @@ class TestPayrollEntry(FrappeTestCase): # enqueue salary slip creation via payroll entry # Payroll Entry status should change to Queued dates = get_start_end_dates("Monthly", nowdate()) - payroll_entry = get_payroll_entry_data( + payroll_entry = get_payroll_entry( start_date=dates.start_date, end_date=dates.end_date, payable_account=company_doc.default_payroll_payable_account, @@ -308,7 +308,7 @@ class TestPayrollEntry(FrappeTestCase): cost_center="Main - _TC", ) frappe.flags.enqueue_payroll_entry = True - payroll_entry.create_salary_slips() + payroll_entry.submit() payroll_entry.reload() self.assertEqual(payroll_entry.status, "Queued") @@ -335,7 +335,7 @@ class TestPayrollEntry(FrappeTestCase): # salary slip submission via payroll entry # Payroll Entry status should change to Failed because of the missing account setup dates = get_start_end_dates("Monthly", nowdate()) - payroll_entry = get_payroll_entry_data( + payroll_entry = get_payroll_entry( start_date=dates.start_date, end_date=dates.end_date, payable_account=company_doc.default_payroll_payable_account, @@ -343,7 +343,16 @@ class TestPayrollEntry(FrappeTestCase): company=company_doc.name, cost_center="Main - _TC", ) - payroll_entry.create_salary_slips() + + # set employee as Inactive to check creation failure + frappe.db.set_value("Employee", employee, "status", "Inactive") + payroll_entry.submit() + payroll_entry.reload() + self.assertEqual(payroll_entry.status, "Failed") + self.assertIsNotNone(payroll_entry.error_message) + + frappe.db.set_value("Employee", employee, "status", "Active") + payroll_entry.submit() payroll_entry.submit_salary_slips() payroll_entry.reload() @@ -369,7 +378,7 @@ class TestPayrollEntry(FrappeTestCase): setup_salary_structure(employee, company_doc) dates = get_start_end_dates("Monthly", nowdate()) - payroll_entry = get_payroll_entry_data( + payroll_entry = get_payroll_entry( start_date=dates.start_date, end_date=dates.end_date, payable_account=company_doc.default_payroll_payable_account, @@ -384,7 +393,7 @@ class TestPayrollEntry(FrappeTestCase): self.assertEqual(payroll_entry.status, "Cancelled") -def get_payroll_entry_data(**args): +def get_payroll_entry(**args): args = frappe._dict(args) payroll_entry = frappe.new_doc("Payroll Entry") @@ -407,13 +416,16 @@ def get_payroll_entry_data(**args): payroll_entry.payment_account = args.payment_account payroll_entry.fill_employee_details() - payroll_entry.save() + payroll_entry.insert() + + # Commit so that the first salary slip creation failure does not rollback the Payroll Entry insert. + frappe.db.commit() # nosemgrep return payroll_entry def make_payroll_entry(**args): - payroll_entry = get_payroll_entry_data(**args) + payroll_entry = get_payroll_entry(**args) payroll_entry.submit() payroll_entry.submit_salary_slips() if payroll_entry.get_sal_slip_list(ss_status=1): From 1db4e623ab6d728ee71663a33a22023961f03ea0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 2 Jun 2022 17:26:08 +0530 Subject: [PATCH 140/192] fix: payroll operations button visibility --- .../doctype/payroll_entry/payroll_entry.js | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js index a33f7665bd..b06f3502e2 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js @@ -40,27 +40,40 @@ frappe.ui.form.on('Payroll Entry', { }, refresh: function (frm) { - if (frm.doc.docstatus == 0) { - if (!frm.is_new()) { + if (frm.doc.docstatus === 0 && !frm.is_new()) { + frm.page.clear_primary_action(); + frm.add_custom_button(__("Get Employees"), + function () { + frm.events.get_employee_details(frm); + } + ).toggleClass("btn-primary", !(frm.doc.employees || []).length); + } + + if ( + (frm.doc.employees || []).length + && !frappe.model.has_workflow(frm.doctype) + && !cint(frm.doc.salary_slips_created) + && (frm.doc.docstatus != 2) + ) { + if (frm.doc.docstatus == 0) { frm.page.clear_primary_action(); - frm.add_custom_button(__("Get Employees"), - function () { - frm.events.get_employee_details(frm); - } - ).toggleClass('btn-primary', !(frm.doc.employees || []).length); - } - if ((frm.doc.employees || []).length && !frappe.model.has_workflow(frm.doctype)) { - frm.page.clear_primary_action(); - frm.page.set_primary_action(__('Create Salary Slips'), () => { - frm.save('Submit').then(() => { + frm.page.set_primary_action(__("Create Salary Slips"), () => { + frm.save("Submit").then(() => { frm.page.clear_primary_action(); frm.refresh(); frm.events.refresh(frm); }); }); + } else if (frm.doc.docstatus == 1 && frm.doc.status == "Failed") { + frm.add_custom_button(__("Create Salary Slip"), function () { + frm.call("create_salary_slips", {}, () => { + frm.reload_doc(); + }); + }).addClass("btn-primary"); } } - if (frm.doc.docstatus == 1) { + + if (frm.doc.docstatus == 1 && frm.doc.status == "Submitted") { if (frm.custom_buttons) frm.clear_custom_buttons(); frm.events.add_context_buttons(frm); } From 3a1c923e76c1f19e101e32d98f54f9ac7d9266bc Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Thu, 2 Jun 2022 14:15:50 -0400 Subject: [PATCH 141/192] fix: display currencies in validation message. --- erpnext/controllers/accounts_controller.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index bebfa6c76f..24a34d6313 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1465,8 +1465,10 @@ class AccountsController(TransactionBase): if not party_gle_currency and (party_account_currency != self.currency): frappe.throw( - _("Party Account {0} currency and document currency should be same").format( - frappe.bold(party_account) + _("Party Account {0} currency ({1}) and document currency ({2}) should be same").format( + frappe.bold(party_account), + party_account_currency, + self.currency ) ) From b061ea4cd2e66a9a0e2a96ef9174f7c0366c52e9 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Thu, 2 Jun 2022 14:23:54 -0400 Subject: [PATCH 142/192] chore: linter --- erpnext/controllers/accounts_controller.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 24a34d6313..854c0d00f5 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1466,9 +1466,7 @@ class AccountsController(TransactionBase): if not party_gle_currency and (party_account_currency != self.currency): frappe.throw( _("Party Account {0} currency ({1}) and document currency ({2}) should be same").format( - frappe.bold(party_account), - party_account_currency, - self.currency + frappe.bold(party_account), party_account_currency, self.currency ) ) From a200e7e1fbb14baf547e47f9644c8b2819916e41 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 5 Jun 2022 11:16:12 +0530 Subject: [PATCH 143/192] fix: Remove redundant query --- .../item_wise_sales_register.py | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index 2e7213f42b..ac70666654 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -443,12 +443,6 @@ def get_grand_total(filters, doctype): ] # nosec -def get_deducted_taxes(): - return frappe.db.sql_list( - "select name from `tabPurchase Taxes and Charges` where add_deduct_tax = 'Deduct'" - ) - - def get_tax_accounts( item_list, columns, @@ -462,6 +456,7 @@ def get_tax_accounts( tax_columns = [] invoice_item_row = {} itemised_tax = {} + add_deduct_tax = "charge_type" tax_amount_precision = ( get_field_precision( @@ -477,13 +472,13 @@ def get_tax_accounts( conditions = "" if doctype == "Purchase Invoice": conditions = " and category in ('Total', 'Valuation and Total') and base_tax_amount_after_discount_amount != 0" + add_deduct_tax = "add_deduct_tax" - deducted_tax = get_deducted_taxes() tax_details = frappe.db.sql( """ select name, parent, description, item_wise_tax_detail, - charge_type, base_tax_amount_after_discount_amount + charge_type, {add_deduct_tax}, base_tax_amount_after_discount_amount from `tab%s` where parenttype = %s and docstatus = 1 @@ -491,12 +486,22 @@ def get_tax_accounts( and parent in (%s) %s order by description - """ + """.format( + add_deduct_tax=add_deduct_tax + ) % (tax_doctype, "%s", ", ".join(["%s"] * len(invoice_item_row)), conditions), tuple([doctype] + list(invoice_item_row)), ) - for name, parent, description, item_wise_tax_detail, charge_type, tax_amount in tax_details: + for ( + name, + parent, + description, + item_wise_tax_detail, + charge_type, + add_deduct_tax, + tax_amount, + ) in tax_details: description = handle_html(description) if description not in tax_columns and tax_amount: # as description is text editor earlier and markup can break the column convention in reports @@ -529,7 +534,9 @@ def get_tax_accounts( if item_tax_amount: tax_value = flt(item_tax_amount, tax_amount_precision) tax_value = ( - tax_value * -1 if (doctype == "Purchase Invoice" and name in deducted_tax) else tax_value + tax_value * -1 + if (doctype == "Purchase Invoice" and add_deduct_tax == "Deduct") + else tax_value ) itemised_tax.setdefault(d.name, {})[description] = frappe._dict( From db07831db781b66a0070212ef5a06a600638aa27 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 5 Jun 2022 12:13:02 +0530 Subject: [PATCH 144/192] fix(India): Supplies from composite dealer not showing up --- erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py index d6210abf80..91fccfa6e8 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -148,7 +148,6 @@ class GSTR3BReport(Document): FROM `tabPurchase Invoice` p , `tabPurchase Invoice Item` i WHERE p.docstatus = 1 and p.name = i.parent and p.is_opening = 'No' - and p.gst_category != 'Registered Composition' and (i.is_nil_exempt = 1 or i.is_non_gst = 1 or p.gst_category = 'Registered Composition') and month(p.posting_date) = %s and year(p.posting_date) = %s and p.company = %s and p.company_gstin = %s From 61fa4eb6c947525a948ec3212e3d7af10eed815f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 5 Jun 2022 18:23:24 +0530 Subject: [PATCH 145/192] fix: Reverse provisional entries on Purchase Invoice cancel --- .../doctype/purchase_invoice/purchase_invoice.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index e6da6669ac..deb905b009 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -532,7 +532,10 @@ class PurchaseInvoice(BuyingController): def make_gl_entries(self, gl_entries=None, from_repost=False): if not gl_entries: - gl_entries = self.get_gl_entries() + if self.docstatus == 1: + gl_entries = self.get_gl_entries() + else: + gl_entries = self.get_gl_entries(cancel=1) if gl_entries: update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes" @@ -545,7 +548,10 @@ class PurchaseInvoice(BuyingController): from_repost=from_repost, ) elif self.docstatus == 2: + provisional_entries = [a for a in gl_entries if a.voucher_type == "Purchase Receipt"] make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) + if provisional_entries: + make_gl_entries(provisional_entries) if update_outstanding == "No": update_outstanding_amt( @@ -559,7 +565,7 @@ class PurchaseInvoice(BuyingController): elif self.docstatus == 2 and cint(self.update_stock) and self.auto_accounting_for_stock: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) - def get_gl_entries(self, warehouse_account=None): + def get_gl_entries(self, warehouse_account=None, cancel=0): self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company) if self.auto_accounting_for_stock: self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed") @@ -572,7 +578,7 @@ class PurchaseInvoice(BuyingController): gl_entries = [] self.make_supplier_gl_entry(gl_entries) - self.make_item_gl_entries(gl_entries) + self.make_item_gl_entries(gl_entries, cancel=cancel) self.make_discount_gl_entries(gl_entries) if self.check_asset_cwip_enabled(): @@ -639,7 +645,7 @@ class PurchaseInvoice(BuyingController): ) ) - def make_item_gl_entries(self, gl_entries): + def make_item_gl_entries(self, gl_entries, cancel=0): # item gl entries stock_items = self.get_stock_items() if self.update_stock and self.auto_accounting_for_stock: @@ -836,7 +842,7 @@ class PurchaseInvoice(BuyingController): if expense_booked_in_pr: # Intentionally passing purchase invoice item to handle partial billing purchase_receipt_doc.add_provisional_gl_entry( - item, gl_entries, self.posting_date, provisional_account, reverse=1 + item, gl_entries, self.posting_date, provisional_account, reverse=not cancel ) if not self.is_internal_transfer(): From 86a24f3d223c0ede3b8e9762bd166285b39a9b10 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 5 Jun 2022 19:12:24 +0530 Subject: [PATCH 146/192] fix: Simply cancel reverse entries --- .../purchase_invoice/purchase_invoice.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index deb905b009..07173a31c2 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -532,10 +532,7 @@ class PurchaseInvoice(BuyingController): def make_gl_entries(self, gl_entries=None, from_repost=False): if not gl_entries: - if self.docstatus == 1: - gl_entries = self.get_gl_entries() - else: - gl_entries = self.get_gl_entries(cancel=1) + gl_entries = self.get_gl_entries() if gl_entries: update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes" @@ -551,7 +548,13 @@ class PurchaseInvoice(BuyingController): provisional_entries = [a for a in gl_entries if a.voucher_type == "Purchase Receipt"] make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) if provisional_entries: - make_gl_entries(provisional_entries) + for entry in provisional_entries: + frappe.db.set_value( + "GL Entry", + {"voucher_type": "Purchase Receipt", "voucher_detail_no": entry.voucher_detail_no}, + "is_cancelled", + 1, + ) if update_outstanding == "No": update_outstanding_amt( @@ -565,7 +568,7 @@ class PurchaseInvoice(BuyingController): elif self.docstatus == 2 and cint(self.update_stock) and self.auto_accounting_for_stock: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) - def get_gl_entries(self, warehouse_account=None, cancel=0): + def get_gl_entries(self, warehouse_account=None): self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company) if self.auto_accounting_for_stock: self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed") @@ -578,7 +581,7 @@ class PurchaseInvoice(BuyingController): gl_entries = [] self.make_supplier_gl_entry(gl_entries) - self.make_item_gl_entries(gl_entries, cancel=cancel) + self.make_item_gl_entries(gl_entries) self.make_discount_gl_entries(gl_entries) if self.check_asset_cwip_enabled(): @@ -645,7 +648,7 @@ class PurchaseInvoice(BuyingController): ) ) - def make_item_gl_entries(self, gl_entries, cancel=0): + def make_item_gl_entries(self, gl_entries): # item gl entries stock_items = self.get_stock_items() if self.update_stock and self.auto_accounting_for_stock: @@ -842,7 +845,7 @@ class PurchaseInvoice(BuyingController): if expense_booked_in_pr: # Intentionally passing purchase invoice item to handle partial billing purchase_receipt_doc.add_provisional_gl_entry( - item, gl_entries, self.posting_date, provisional_account, reverse=not cancel + item, gl_entries, self.posting_date, provisional_account, reverse=1 ) if not self.is_internal_transfer(): From c3219ebad1cac35afc04cc051c9e215c70cd1e9b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 6 Jun 2022 12:39:12 +0530 Subject: [PATCH 147/192] fix(Sales Register): incorrect query with dimensions If accounting dimension is also part of the default filters then same query is repeated with incorrect syntax. e.g. `item_group = (child1, child2)` instead of `in` query. fix: don't add default filter if they are part of dimensions to be added. --- .../report/sales_register/sales_register.py | 40 +++++++------------ 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py index 34b3f03206..777d96ced1 100644 --- a/erpnext/accounts/report/sales_register/sales_register.py +++ b/erpnext/accounts/report/sales_register/sales_register.py @@ -346,9 +346,13 @@ def get_columns(invoice_list, additional_table_columns): def get_conditions(filters): conditions = "" + accounting_dimensions = get_accounting_dimensions(as_list=False) or [] + accounting_dimensions_list = [d.fieldname for d in accounting_dimensions] + if filters.get("company"): conditions += " and company=%(company)s" - if filters.get("customer"): + + if filters.get("customer") and "customer" not in accounting_dimensions_list: conditions += " and customer = %(customer)s" if filters.get("from_date"): @@ -359,32 +363,18 @@ def get_conditions(filters): if filters.get("owner"): conditions += " and owner = %(owner)s" - if filters.get("mode_of_payment"): - conditions += """ and exists(select name from `tabSales Invoice Payment` + def get_sales_invoice_item_field_condition(field, table="Sales Invoice Item") -> str: + if not filters.get(field) or field in accounting_dimensions_list: + return "" + return f""" and exists(select name from `tab{table}` where parent=`tabSales Invoice`.name - and ifnull(`tabSales Invoice Payment`.mode_of_payment, '') = %(mode_of_payment)s)""" + and ifnull(`tab{table}`.{field}, '') = %({field})s)""" - if filters.get("cost_center"): - conditions += """ and exists(select name from `tabSales Invoice Item` - where parent=`tabSales Invoice`.name - and ifnull(`tabSales Invoice Item`.cost_center, '') = %(cost_center)s)""" - - if filters.get("warehouse"): - conditions += """ and exists(select name from `tabSales Invoice Item` - where parent=`tabSales Invoice`.name - and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s)""" - - if filters.get("brand"): - conditions += """ and exists(select name from `tabSales Invoice Item` - where parent=`tabSales Invoice`.name - and ifnull(`tabSales Invoice Item`.brand, '') = %(brand)s)""" - - if filters.get("item_group"): - conditions += """ and exists(select name from `tabSales Invoice Item` - where parent=`tabSales Invoice`.name - and ifnull(`tabSales Invoice Item`.item_group, '') = %(item_group)s)""" - - accounting_dimensions = get_accounting_dimensions(as_list=False) + conditions += get_sales_invoice_item_field_condition("mode_of_payments", "Sales Invoice Payment") + conditions += get_sales_invoice_item_field_condition("cost_center") + conditions += get_sales_invoice_item_field_condition("warehouse") + conditions += get_sales_invoice_item_field_condition("brand") + conditions += get_sales_invoice_item_field_condition("item_group") if accounting_dimensions: common_condition = """ From 4decb7a02be94a98ad8dea9d5d4db37f7410b1d2 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 3 Jun 2022 11:03:12 +0530 Subject: [PATCH 148/192] fix: Consider only Approved leave applications in LWP, Employee Benefit calculations - do not allow submitting leave applications with 'Cancelled' status --- erpnext/hr/doctype/leave_application/leave_application.py | 5 +++-- .../employee_benefit_application.py | 1 + erpnext/payroll/doctype/salary_slip/salary_slip.py | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index cd6b168667..5a338bdfe8 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -88,7 +88,7 @@ class LeaveApplication(Document): share_doc_with_approver(self, self.leave_approver) def on_submit(self): - if self.status == "Open": + if self.status in ["Open", "Cancelled"]: frappe.throw( _("Only Leave Applications with status 'Approved' and 'Rejected' can be submitted") ) @@ -1117,7 +1117,7 @@ def add_leaves(events, start, end, filter_conditions=None): WHERE from_date <= %(end)s AND to_date >= %(start)s <= to_date AND docstatus < 2 - AND status != 'Rejected' + AND status in ('Approved', 'Open') """ if conditions: @@ -1206,6 +1206,7 @@ def get_approved_leaves_for_period(employee, leave_type, from_date, to_date): from `tabLeave Application` where employee=%(employee)s and docstatus=1 + and status='Approved' and (from_date between %(from_date)s and %(to_date)s or to_date between %(from_date)s and %(to_date)s or (from_date < %(from_date)s and to_date > %(to_date)s)) diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py index 0acd44711b..592e7dd6f0 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py @@ -216,6 +216,7 @@ def calculate_lwp(employee, start_date, holidays, working_days): where t2.name = t1.leave_type and t2.is_lwp = 1 and t1.docstatus = 1 + and t1.status = 'Approved' and t1.employee = %(employee)s and CASE WHEN t2.include_holiday != 1 THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date WHEN t2.include_holiday THEN %(dt)s between from_date and to_date diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 4c5fea1e75..378227f046 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -477,6 +477,7 @@ class SalarySlip(TransactionBase): WHERE t2.name = t1.leave_type AND (t2.is_lwp = 1 or t2.is_ppl = 1) AND t1.docstatus = 1 + AND t1.status = 'Approved' AND t1.employee = %(employee)s AND ifnull(t1.salary_slip, '') = '' AND CASE From 59de303b131c49f5ffacb3142f58b688e2d1c926 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 3 Jun 2022 16:47:42 +0530 Subject: [PATCH 149/192] refactor: rewrite lwp queries using query builder --- .../leave_application/leave_application.py | 42 ++++++---- .../employee_benefit_application.py | 44 ++++++----- .../doctype/salary_slip/salary_slip.py | 77 ++++++++++++------- 3 files changed, 99 insertions(+), 64 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 5a338bdfe8..53c5df4210 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -1201,25 +1201,33 @@ def get_mandatory_approval(doctype): def get_approved_leaves_for_period(employee, leave_type, from_date, to_date): - query = """ - select employee, leave_type, from_date, to_date, total_leave_days - from `tabLeave Application` - where employee=%(employee)s - and docstatus=1 - and status='Approved' - and (from_date between %(from_date)s and %(to_date)s - or to_date between %(from_date)s and %(to_date)s - or (from_date < %(from_date)s and to_date > %(to_date)s)) - """ - if leave_type: - query += "and leave_type=%(leave_type)s" - - leave_applications = frappe.db.sql( - query, - {"from_date": from_date, "to_date": to_date, "employee": employee, "leave_type": leave_type}, - as_dict=1, + LeaveApplication = frappe.qb.DocType("Leave Application") + query = ( + frappe.qb.from_(LeaveApplication) + .select( + LeaveApplication.employee, + LeaveApplication.leave_type, + LeaveApplication.from_date, + LeaveApplication.to_date, + LeaveApplication.total_leave_days, + ) + .where( + (LeaveApplication.employee == employee) + & (LeaveApplication.docstatus == 1) + & (LeaveApplication.status == "Approved") + & ( + (LeaveApplication.from_date.between(from_date, to_date)) + | (LeaveApplication.to_date.between(from_date, to_date)) + | ((LeaveApplication.from_date < from_date) & (LeaveApplication.to_date > to_date)) + ) + ) ) + if leave_type: + query = query.where(LeaveApplication.leave_type == leave_type) + + leave_applications = query.run(as_dict=True) + leave_days = 0 for leave_app in leave_applications: if leave_app.from_date >= getdate(from_date) and leave_app.to_date <= getdate(to_date): diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py index 592e7dd6f0..8dad7cc8bc 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py @@ -207,27 +207,35 @@ def get_max_benefits_remaining(employee, on_date, payroll_period): def calculate_lwp(employee, start_date, holidays, working_days): lwp = 0 holidays = "','".join(holidays) + for d in range(working_days): dt = add_days(cstr(getdate(start_date)), d) - leave = frappe.db.sql( - """ - select t1.name, t1.half_day - from `tabLeave Application` t1, `tabLeave Type` t2 - where t2.name = t1.leave_type - and t2.is_lwp = 1 - and t1.docstatus = 1 - and t1.status = 'Approved' - and t1.employee = %(employee)s - and CASE WHEN t2.include_holiday != 1 THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date - WHEN t2.include_holiday THEN %(dt)s between from_date and to_date - END - """.format( - holidays - ), - {"employee": employee, "dt": dt}, + + LeaveApplication = frappe.qb.DocType("Leave Application") + LeaveType = frappe.qb.DocType("Leave Type") + + query = ( + frappe.qb.from_(LeaveApplication) + .inner_join(LeaveType) + .on((LeaveType.name == LeaveApplication.leave_type)) + .select(LeaveApplication.name, LeaveApplication.half_day) + .where( + (LeaveType.is_lwp == 1) + & (LeaveApplication.docstatus == 1) + & (LeaveApplication.status == "Approved") + & (LeaveApplication.employee == employee) + & ((LeaveApplication.from_date <= dt) & (dt <= LeaveApplication.to_date)) + ) ) - if leave: - lwp = cint(leave[0][1]) and (lwp + 0.5) or (lwp + 1) + + # if it's a holiday only include if leave type has "include holiday" enabled + if dt in holidays: + query = query.where((LeaveType.include_holiday == "1")) + leaves = query.run() + + if leaves: + lwp = cint(leaves[0][1]) and (lwp + 0.5) or (lwp + 1) + return lwp diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 378227f046..6a35985e64 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -465,38 +465,14 @@ class SalarySlip(TransactionBase): ) for d in range(working_days): - dt = add_days(cstr(getdate(self.start_date)), d) - leave = frappe.db.sql( - """ - SELECT t1.name, - CASE WHEN (t1.half_day_date = %(dt)s or t1.to_date = t1.from_date) - THEN t1.half_day else 0 END, - t2.is_ppl, - t2.fraction_of_daily_salary_per_leave - FROM `tabLeave Application` t1, `tabLeave Type` t2 - WHERE t2.name = t1.leave_type - AND (t2.is_lwp = 1 or t2.is_ppl = 1) - AND t1.docstatus = 1 - AND t1.status = 'Approved' - AND t1.employee = %(employee)s - AND ifnull(t1.salary_slip, '') = '' - AND CASE - WHEN t2.include_holiday != 1 - THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date - WHEN t2.include_holiday - THEN %(dt)s between from_date and to_date - END - """.format( - holidays - ), - {"employee": self.employee, "dt": dt}, - ) + date = add_days(cstr(getdate(self.start_date)), d) + leave = get_lwp_or_ppl_for_date(date, self.employee, holidays) if leave: equivalent_lwp_count = 0 - is_half_day_leave = cint(leave[0][1]) - is_partially_paid_leave = cint(leave[0][2]) - fraction_of_daily_salary_per_leave = flt(leave[0][3]) + is_half_day_leave = cint(leave[0].is_half_day) + is_partially_paid_leave = cint(leave[0].is_ppl) + fraction_of_daily_salary_per_leave = flt(leave[0].fraction_of_daily_salary_per_leave) equivalent_lwp_count = (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1 @@ -1743,3 +1719,46 @@ def eval_tax_slab_condition(condition, eval_globals=None, eval_locals=None): except Exception as e: frappe.throw(_("Error in formula or condition: {0} in Income Tax Slab").format(e)) raise + + +def get_lwp_or_ppl_for_date(date, employee, holidays): + LeaveApplication = frappe.qb.DocType("Leave Application") + LeaveType = frappe.qb.DocType("Leave Type") + + is_half_day = ( + frappe.qb.terms.Case() + .when( + ( + (LeaveApplication.half_day_date == date) + | (LeaveApplication.from_date == LeaveApplication.to_date) + ), + LeaveApplication.half_day, + ) + .else_(0) + ).as_("is_half_day") + + query = ( + frappe.qb.from_(LeaveApplication) + .inner_join(LeaveType) + .on((LeaveType.name == LeaveApplication.leave_type)) + .select( + LeaveApplication.name, + LeaveType.is_ppl, + LeaveType.fraction_of_daily_salary_per_leave, + (is_half_day), + ) + .where( + (((LeaveType.is_lwp == 1) | (LeaveType.is_ppl == 1))) + & (LeaveApplication.docstatus == 1) + & (LeaveApplication.status == "Approved") + & (LeaveApplication.employee == employee) + & ((LeaveApplication.salary_slip.isnull()) | (LeaveApplication.salary_slip == "")) + & ((LeaveApplication.from_date <= date) & (date <= LeaveApplication.to_date)) + ) + ) + + # if it's a holiday only include if leave type has "include holiday" enabled + if date in holidays: + query = query.where((LeaveType.include_holiday == "1")) + + return query.run(as_dict=True) From cf04683ad333ca1d1b34d4bfb9a64b69e539657a Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 3 Jun 2022 17:00:01 +0530 Subject: [PATCH 150/192] fix(Leave Application): 'Cancelled' status shown as 'Open' in list view --- .../leave_application/leave_application_list.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application_list.js b/erpnext/hr/doctype/leave_application/leave_application_list.js index a3c03b1bec..157271a5a0 100644 --- a/erpnext/hr/doctype/leave_application/leave_application_list.js +++ b/erpnext/hr/doctype/leave_application/leave_application_list.js @@ -1,13 +1,14 @@ -frappe.listview_settings['Leave Application'] = { +frappe.listview_settings["Leave Application"] = { add_fields: ["leave_type", "employee", "employee_name", "total_leave_days", "from_date", "to_date"], has_indicator_for_draft: 1, get_indicator: function (doc) { - if (doc.status === "Approved") { - return [__("Approved"), "green", "status,=,Approved"]; - } else if (doc.status === "Rejected") { - return [__("Rejected"), "red", "status,=,Rejected"]; - } else { - return [__("Open"), "red", "status,=,Open"]; - } + let status_color = { + "Approved": "green", + "Rejected": "red", + "Open": "orange", + "Cancelled": "red", + "Submitted": "blue" + }; + return [__(doc.status), status_color[doc.status], "status,=," + doc.status]; } }; From 3b8bc7d8e1446d8b48f7f95924842650cb3f74e3 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 6 Jun 2022 13:03:05 +0530 Subject: [PATCH 151/192] fix: incorrect LWP calculation for half days in employee benefit application --- .../employee_benefit_application.py | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py index 8dad7cc8bc..8df1bb6e87 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py @@ -5,7 +5,7 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import add_days, cint, cstr, date_diff, getdate, rounded +from frappe.utils import add_days, cstr, date_diff, flt, getdate, rounded from erpnext.hr.utils import ( get_holiday_dates_for_employee, @@ -27,11 +27,14 @@ class EmployeeBenefitApplication(Document): validate_active_employee(self.employee) self.validate_duplicate_on_payroll_period() if not self.max_benefits: - self.max_benefits = get_max_benefits_remaining(self.employee, self.date, self.payroll_period) + self.max_benefits = flt( + get_max_benefits_remaining(self.employee, self.date, self.payroll_period), + self.precision("max_benefits"), + ) if self.max_benefits and self.max_benefits > 0: self.validate_max_benefit_for_component() self.validate_prev_benefit_claim() - if self.remaining_benefit > 0: + if self.remaining_benefit and self.remaining_benefit > 0: self.validate_remaining_benefit_amount() else: frappe.throw( @@ -110,7 +113,7 @@ class EmployeeBenefitApplication(Document): max_benefit_amount = 0 for employee_benefit in self.employee_benefits: self.validate_max_benefit(employee_benefit.earning_component) - max_benefit_amount += employee_benefit.amount + max_benefit_amount += flt(employee_benefit.amount) if max_benefit_amount > self.max_benefits: frappe.throw( _("Maximum benefit amount of employee {0} exceeds {1}").format( @@ -125,7 +128,8 @@ class EmployeeBenefitApplication(Document): benefit_amount = 0 for employee_benefit in self.employee_benefits: if employee_benefit.earning_component == earning_component_name: - benefit_amount += employee_benefit.amount + benefit_amount += flt(employee_benefit.amount) + prev_sal_slip_flexi_amount = get_sal_slip_total_benefit_given( self.employee, frappe.get_doc("Payroll Period", self.payroll_period), earning_component_name ) @@ -209,32 +213,44 @@ def calculate_lwp(employee, start_date, holidays, working_days): holidays = "','".join(holidays) for d in range(working_days): - dt = add_days(cstr(getdate(start_date)), d) + date = add_days(cstr(getdate(start_date)), d) LeaveApplication = frappe.qb.DocType("Leave Application") LeaveType = frappe.qb.DocType("Leave Type") + is_half_day = ( + frappe.qb.terms.Case() + .when( + ( + (LeaveApplication.half_day_date == date) + | (LeaveApplication.from_date == LeaveApplication.to_date) + ), + LeaveApplication.half_day, + ) + .else_(0) + ).as_("is_half_day") + query = ( frappe.qb.from_(LeaveApplication) .inner_join(LeaveType) .on((LeaveType.name == LeaveApplication.leave_type)) - .select(LeaveApplication.name, LeaveApplication.half_day) + .select(LeaveApplication.name, is_half_day) .where( (LeaveType.is_lwp == 1) & (LeaveApplication.docstatus == 1) & (LeaveApplication.status == "Approved") & (LeaveApplication.employee == employee) - & ((LeaveApplication.from_date <= dt) & (dt <= LeaveApplication.to_date)) + & ((LeaveApplication.from_date <= date) & (date <= LeaveApplication.to_date)) ) ) # if it's a holiday only include if leave type has "include holiday" enabled - if dt in holidays: + if date in holidays: query = query.where((LeaveType.include_holiday == "1")) - leaves = query.run() + leaves = query.run(as_dict=True) if leaves: - lwp = cint(leaves[0][1]) and (lwp + 0.5) or (lwp + 1) + lwp += 0.5 if leaves[0].is_half_day else 1 return lwp From edb775c3814ec94537f790d92b745e9672a45f21 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 6 Jun 2022 13:42:23 +0530 Subject: [PATCH 152/192] test: Employee Benefit Application - make `get_no_of_days` a function for reusability --- .../test_employee_benefit_application.py | 80 ++++++++++++++++++- .../doctype/salary_slip/test_salary_slip.py | 40 +++++----- 2 files changed, 99 insertions(+), 21 deletions(-) diff --git a/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.py index 02149adfce..de8f9b6a7a 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.py +++ b/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.py @@ -3,6 +3,82 @@ import unittest +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days, date_diff, get_year_ending, get_year_start, getdate -class TestEmployeeBenefitApplication(unittest.TestCase): - pass +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list +from erpnext.hr.doctype.leave_application.test_leave_application import get_first_sunday +from erpnext.hr.utils import get_holiday_dates_for_employee +from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import ( + calculate_lwp, +) +from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import ( + create_payroll_period, +) +from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( + make_holiday_list, + make_leave_application, +) +from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip +from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure + + +class TestEmployeeBenefitApplication(FrappeTestCase): + def setUp(self): + date = getdate() + make_holiday_list(from_date=get_year_start(date), to_date=get_year_ending(date)) + + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") + def test_employee_benefit_application(self): + payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") + employee = make_employee("test_employee_benefits@salary.com", company="_Test Company") + first_sunday = get_first_sunday("Salary Slip Test Holiday List") + + leave_application = make_leave_application( + employee, + add_days(first_sunday, 1), + add_days(first_sunday, 3), + "Leave Without Pay", + half_day=1, + half_day_date=add_days(first_sunday, 1), + submit=True, + ) + + frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0) + salary_structure = make_salary_structure( + "Test Employee Benefits", + "Monthly", + other_details={"max_benefits": 100000}, + include_flexi_benefits=True, + employee=employee, + payroll_period=payroll_period, + ) + salary_slip = make_salary_slip(salary_structure.name, employee=employee, posting_date=getdate()) + salary_slip.insert() + salary_slip.submit() + + application = make_employee_benefit_application( + employee, payroll_period.name, date=leave_application.to_date + ) + self.assertEqual(application.employee_benefits[0].max_benefit_amount, 15000) + + holidays = get_holiday_dates_for_employee(employee, payroll_period.start_date, application.date) + working_days = date_diff(application.date, payroll_period.start_date) + 1 + lwp = calculate_lwp(employee, payroll_period.start_date, holidays, working_days) + self.assertEqual(lwp, 2.5) + + +def make_employee_benefit_application(employee, payroll_period, date): + frappe.db.delete("Employee Benefit Application") + + return frappe.get_doc( + { + "doctype": "Employee Benefit Application", + "employee": employee, + "date": date, + "payroll_period": payroll_period, + "employee_benefits": [{"earning_component": "Medical Allowance", "amount": 1500}], + } + ).insert() diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 5e3814b73c..a8b6bb5714 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -49,7 +49,7 @@ class TestSalarySlip(unittest.TestCase): "Payroll Settings", {"payroll_based_on": "Attendance", "daily_wages_fraction_for_half_day": 0.75} ) def test_payment_days_based_on_attendance(self): - no_of_days = self.get_no_of_days() + no_of_days = get_no_of_days() emp_id = make_employee("test_payment_days_based_on_attendance@salary.com") frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"}) @@ -128,7 +128,7 @@ class TestSalarySlip(unittest.TestCase): }, ) def test_payment_days_for_mid_joinee_including_holidays(self): - no_of_days = self.get_no_of_days() + no_of_days = get_no_of_days() month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate()) new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com") @@ -196,7 +196,7 @@ class TestSalarySlip(unittest.TestCase): # tests mid month joining and relieving along with unmarked days from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday - no_of_days = self.get_no_of_days() + no_of_days = get_no_of_days() month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate()) new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com") @@ -236,7 +236,7 @@ class TestSalarySlip(unittest.TestCase): def test_payment_days_for_mid_joinee_excluding_holidays(self): from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday - no_of_days = self.get_no_of_days() + no_of_days = get_no_of_days() month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate()) new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com") @@ -267,7 +267,7 @@ class TestSalarySlip(unittest.TestCase): @change_settings("Payroll Settings", {"payroll_based_on": "Leave"}) def test_payment_days_based_on_leave_application(self): - no_of_days = self.get_no_of_days() + no_of_days = get_no_of_days() emp_id = make_employee("test_payment_days_based_on_leave_application@salary.com") frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"}) @@ -366,7 +366,7 @@ class TestSalarySlip(unittest.TestCase): salary_slip.submit() salary_slip.reload() - no_of_days = self.get_no_of_days() + no_of_days = get_no_of_days() days_in_month = no_of_days[0] no_of_holidays = no_of_days[1] @@ -441,7 +441,7 @@ class TestSalarySlip(unittest.TestCase): @change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 1}) def test_salary_slip_with_holidays_included(self): - no_of_days = self.get_no_of_days() + no_of_days = get_no_of_days() make_employee("test_salary_slip_with_holidays_included@salary.com") frappe.db.set_value( "Employee", @@ -473,7 +473,7 @@ class TestSalarySlip(unittest.TestCase): @change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 0}) def test_salary_slip_with_holidays_excluded(self): - no_of_days = self.get_no_of_days() + no_of_days = get_no_of_days() make_employee("test_salary_slip_with_holidays_excluded@salary.com") frappe.db.set_value( "Employee", @@ -510,7 +510,7 @@ class TestSalarySlip(unittest.TestCase): create_salary_structure_assignment, ) - no_of_days = self.get_no_of_days() + no_of_days = get_no_of_days() # set joinng date in the same month employee = make_employee("test_payment_days@salary.com") @@ -984,17 +984,18 @@ class TestSalarySlip(unittest.TestCase): activity_type.wage_rate = 25 activity_type.save() - def get_no_of_days(self): - no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, getdate(nowdate()).month) - no_of_holidays_in_month = len( - [ - 1 - for i in calendar.monthcalendar(getdate(nowdate()).year, getdate(nowdate()).month) - if i[6] != 0 - ] - ) - return [no_of_days_in_month[1], no_of_holidays_in_month] +def get_no_of_days(): + no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, getdate(nowdate()).month) + no_of_holidays_in_month = len( + [ + 1 + for i in calendar.monthcalendar(getdate(nowdate()).year, getdate(nowdate()).month) + if i[6] != 0 + ] + ) + + return [no_of_days_in_month[1], no_of_holidays_in_month] def make_employee_salary_slip(user, payroll_frequency, salary_structure=None, posting_date=None): @@ -1136,6 +1137,7 @@ def make_earning_salary_component( "pay_against_benefit_claim": 0, "type": "Earning", "max_benefit_amount": 15000, + "depends_on_payment_days": 1, }, ] ) From c66c1e2215d251a904816436c8f99ebfbd83ff65 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 6 Jun 2022 15:27:29 +0530 Subject: [PATCH 153/192] chore(meta): disable stale bot on issues --- .github/stale.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/stale.yml b/.github/stale.yml index fbf6447156..cdce0aee08 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -23,15 +23,3 @@ pulls: activity occurs, but it only takes a comment to keep a contribution alive :) Also, even if it is closed, you can always reopen the PR when you're ready. Thank you for contributing. - -issues: - daysUntilStale: 90 - daysUntilClose: 7 - exemptLabels: - - valid - - to-validate - - QA - markComment: > - This issue has been automatically marked as inactive because it has not had - recent activity and it wasn't validated by maintainer team. It will be - closed within a week if no further activity occurs. From 3513d54c0a04887422caf932071754fa71e1f6bc Mon Sep 17 00:00:00 2001 From: vishdha Date: Fri, 3 Jun 2022 14:23:53 +0530 Subject: [PATCH 154/192] fix: Print/PDF for financial statement reports displays either wrong date range or wrong fiscal year --- .../consolidated_financial_statement.js | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js index d3e836afd1..a5c5a67613 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js @@ -50,7 +50,15 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "fieldtype": "Link", "options": "Fiscal Year", "default": frappe.defaults.get_user_default("fiscal_year"), - "reqd": 1 + "reqd": 1, + on_change: () => { + frappe.model.with_doc("Fiscal Year", frappe.query_report.get_filter_value('from_fiscal_year'), function(r) { + let start_fy = frappe.model.get_doc("Fiscal Year", frappe.query_report.get_filter_value('from_fiscal_year')); + frappe.query_report.set_filter_value({ + period_start_date: start_fy.year_start_date + }); + }); + } }, { "fieldname":"to_fiscal_year", @@ -58,7 +66,15 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "fieldtype": "Link", "options": "Fiscal Year", "default": frappe.defaults.get_user_default("fiscal_year"), - "reqd": 1 + "reqd": 1, + on_change: () => { + frappe.model.with_doc("Fiscal Year", frappe.query_report.get_filter_value('to_fiscal_year'), function(r) { + let to_fy = frappe.model.get_doc("Fiscal Year", frappe.query_report.get_filter_value('to_fiscal_year')); + frappe.query_report.set_filter_value({ + period_end_date: to_fy.year_end_date + }); + }); + } }, { "fieldname":"finance_book", From 53774e0f520defeb47d41d2fe3078ab26962f5a6 Mon Sep 17 00:00:00 2001 From: vishdha Date: Mon, 6 Jun 2022 13:07:39 +0530 Subject: [PATCH 155/192] chore: minor change in fetching start and end date --- .../consolidated_financial_statement.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js index a5c5a67613..dd965a9813 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js @@ -53,9 +53,9 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "reqd": 1, on_change: () => { frappe.model.with_doc("Fiscal Year", frappe.query_report.get_filter_value('from_fiscal_year'), function(r) { - let start_fy = frappe.model.get_doc("Fiscal Year", frappe.query_report.get_filter_value('from_fiscal_year')); + let year_start_date = frappe.model.get_value("Fiscal Year", frappe.query_report.get_filter_value('from_fiscal_year'), "year_start_date"); frappe.query_report.set_filter_value({ - period_start_date: start_fy.year_start_date + period_start_date: year_start_date }); }); } @@ -69,9 +69,9 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "reqd": 1, on_change: () => { frappe.model.with_doc("Fiscal Year", frappe.query_report.get_filter_value('to_fiscal_year'), function(r) { - let to_fy = frappe.model.get_doc("Fiscal Year", frappe.query_report.get_filter_value('to_fiscal_year')); + let year_end_date = frappe.model.get_value("Fiscal Year", frappe.query_report.get_filter_value('to_fiscal_year'), "year_end_date"); frappe.query_report.set_filter_value({ - period_end_date: to_fy.year_end_date + period_end_date: year_end_date }); }); } From ee5bc58e9ba8b4c4b4ab255101919974302068e6 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 6 Jun 2022 16:27:25 +0530 Subject: [PATCH 156/192] fix(job card): only hold during draft state (#31243) --- .../doctype/job_card/job_card.py | 2 +- erpnext/patches.txt | 1 + .../patches/v13_0/job_card_status_on_hold.py | 19 +++++++++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v13_0/job_card_status_on_hold.py diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 0a9fd8a099..0199a5c31e 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -621,7 +621,7 @@ class JobCard(Document): self.set_status(update_status) def set_status(self, update_status=False): - if self.status == "On Hold": + if self.status == "On Hold" and self.docstatus == 0: return self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0] diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 8594ebbe9d..5a984635fd 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -373,3 +373,4 @@ erpnext.patches.v13_0.create_accounting_dimensions_in_orders erpnext.patches.v13_0.set_per_billed_in_return_delivery_note execute:frappe.delete_doc("DocType", "Naming Series") erpnext.patches.v13_0.set_payroll_entry_status +erpnext.patches.v13_0.job_card_status_on_hold diff --git a/erpnext/patches/v13_0/job_card_status_on_hold.py b/erpnext/patches/v13_0/job_card_status_on_hold.py new file mode 100644 index 0000000000..8c67c3c858 --- /dev/null +++ b/erpnext/patches/v13_0/job_card_status_on_hold.py @@ -0,0 +1,19 @@ +import frappe + + +def execute(): + job_cards = frappe.get_all( + "Job Card", + {"status": "On Hold", "docstatus": ("!=", 0)}, + pluck="name", + ) + + for idx, job_card in enumerate(job_cards): + try: + doc = frappe.get_doc("Job Card", job_card) + doc.set_status() + doc.db_set("status", doc.status, update_modified=False) + if idx % 100 == 0: + frappe.db.commit() + except Exception: + continue From 934db57fdd5346dde982e9022f33d61780175d07 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 6 Jun 2022 17:01:51 +0530 Subject: [PATCH 157/192] chore: Miscellanous fixes/enhancements - `get_valuation_rate`: if no bins are found return 0, SLEs do not exist either - `get_valuation_rate`: Compute average valuation rate via query - `get_rm_rate_map`: set order_by as None to avoid creating sort index (modified) each time query runs (seen in process list) - BOM Update Batch: add status field and hide `boms_updated` so that users can see progress without loading all updated boms (too much data) - BOM Update Batch: set batch row status to completed after job runs - BOM Update Log: remove `parent_boms` field (just pass parent boms to processing function) & remove Paused state (not used) - Move job to long queue to avoid choking default queue - `update_cost_in_boms`: use `get_doc` as each BOM is accessed only once. Use `for_update` to lock BOM row - Commit after every 100 BOMs --- erpnext/manufacturing/doctype/bom/bom.py | 30 +++++++++------- .../bom_update_batch/bom_update_batch.json | 14 ++++++-- .../bom_update_log/bom_update_log.json | 12 ++----- .../doctype/bom_update_log/bom_update_log.py | 34 ++++++++++++------- .../bom_update_log/bom_updation_utils.py | 24 +++++++++---- .../bom_update_tool/bom_update_tool.py | 2 +- 6 files changed, 73 insertions(+), 43 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index d4e0d4b814..1fb00ef3fe 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -714,8 +714,11 @@ class BOM(WebsiteGenerator): for item in self.get("items"): if item.bom_no: # Get Item-Rate from Subassembly BOM - explosion_items = frappe.db.get_all( - "BOM Explosion Item", filters={"parent": item.bom_no}, fields=["item_code", "rate"] + explosion_items = frappe.get_all( + "BOM Explosion Item", + filters={"parent": item.bom_no}, + fields=["item_code", "rate"], + order_by=None, # to avoid sort index creation at db level (granular change) ) explosion_item_rate = {item.item_code: flt(item.rate) for item in explosion_items} rm_rate_map.update(explosion_item_rate) @@ -935,13 +938,17 @@ def get_bom_item_rate(args, bom_doc): def get_valuation_rate(args): - """Get weighted average of valuation rate from all warehouses""" + """ + 1) Get average valuation rate from all warehouses + 2) If no value, get last valuation rate from SLE + 3) If no value, get valuation rate from Item + """ - total_qty, total_value, valuation_rate = 0.0, 0.0, 0.0 - item_bins = frappe.db.sql( + valuation_rate = 0.0 + item_valuation = frappe.db.sql( """ select - bin.actual_qty, bin.stock_value + (sum(bin.stock_value) / sum(bin.actual_qty)) as valuation_rate from `tabBin` bin, `tabWarehouse` warehouse where @@ -950,14 +957,13 @@ def get_valuation_rate(args): and warehouse.company=%(company)s""", {"item": args["item_code"], "company": args["company"]}, as_dict=1, - ) + )[0] - for d in item_bins: - total_qty += flt(d.actual_qty) - total_value += flt(d.stock_value) + valuation_rate = item_valuation.get("valuation_rate") - if total_qty: - valuation_rate = total_value / total_qty + if valuation_rate is None: + # Explicit null value check. If null, Bins don't exist, neither does SLE + return valuation_rate if valuation_rate <= 0: last_valuation_rate = frappe.db.sql( diff --git a/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json b/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json index 9938454ce4..83b54d326c 100644 --- a/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json +++ b/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json @@ -7,7 +7,8 @@ "field_order": [ "level", "batch_no", - "boms_updated" + "boms_updated", + "status" ], "fields": [ { @@ -25,14 +26,23 @@ { "fieldname": "boms_updated", "fieldtype": "Long Text", + "hidden": 1, "in_list_view": 1, "label": "BOMs Updated" + }, + { + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "options": "Pending\nCompleted", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-05-31 23:36:13.628391", + "modified": "2022-06-06 14:50:35.161062", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Update Batch", diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json index b1c24ab995..c32e383b08 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json @@ -15,7 +15,6 @@ "error_log", "progress_section", "current_level", - "parent_boms", "processed_boms", "bom_batches", "amended_from" @@ -52,7 +51,7 @@ "fieldname": "status", "fieldtype": "Select", "label": "Status", - "options": "Queued\nIn Progress\nPaused\nCompleted\nFailed" + "options": "Queued\nIn Progress\nCompleted\nFailed" }, { "fieldname": "amended_from", @@ -76,15 +75,10 @@ "fieldtype": "Section Break", "label": "Progress" }, - { - "description": "Immediate parent BOMs", - "fieldname": "parent_boms", - "fieldtype": "Long Text", - "label": "Parent BOMs" - }, { "fieldname": "processed_boms", "fieldtype": "Long Text", + "hidden": 1, "label": "Processed BOMs" }, { @@ -102,7 +96,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-05-31 20:20:06.370786", + "modified": "2022-06-06 15:15:23.883251", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Update Log", diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index bfae76c2b2..d714b9d5fd 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -56,7 +56,7 @@ class BOMUpdateLog(Document): wip_log = frappe.get_all( "BOM Update Log", - {"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress", "Paused"]]}, + {"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress"]]}, limit_page_length=1, ) if wip_log: @@ -104,10 +104,12 @@ def run_replace_bom_job( frappe.db.commit() # nosemgrep -def process_boms_cost_level_wise(update_doc: "BOMUpdateLog") -> None: +def process_boms_cost_level_wise( + update_doc: "BOMUpdateLog", parent_boms: List[str] = None +) -> None: "Queue jobs at the start of new BOM Level in 'Update Cost' Jobs." - current_boms, parent_boms = {}, [] + current_boms = {} values = {} if update_doc.status == "Queued": @@ -115,26 +117,27 @@ def process_boms_cost_level_wise(update_doc: "BOMUpdateLog") -> None: current_level = 0 current_boms = get_leaf_boms() values = { - "parent_boms": "[]", "processed_boms": json.dumps({}), "status": "In Progress", "current_level": current_level, } else: # Resume next level. via Cron Job. + if not parent_boms: + return + current_level = cint(update_doc.current_level) + 1 - parent_boms = json.loads(update_doc.parent_boms) # Process the next level BOMs. Stage parents as current BOMs. current_boms = parent_boms.copy() - values = {"parent_boms": "[]", "current_level": current_level} + values = {"current_level": current_level} set_values_in_log(update_doc.name, values, commit=True) queue_bom_cost_jobs(current_boms, update_doc, current_level) def queue_bom_cost_jobs( - current_boms_list: List, update_doc: "BOMUpdateLog", current_level: int + current_boms_list: List[str], update_doc: "BOMUpdateLog", current_level: int ) -> None: "Queue batches of 20k BOMs of the same level to process parallelly" batch_no = 0 @@ -147,7 +150,9 @@ def queue_bom_cost_jobs( # update list to exclude 20K (queued) BOMs current_boms_list = current_boms_list[batch_size:] if len(current_boms_list) > batch_size else [] - batch_row = update_doc.append("bom_batches", {"level": current_level, "batch_no": batch_no}) + batch_row = update_doc.append( + "bom_batches", {"level": current_level, "batch_no": batch_no, "status": "Pending"} + ) batch_row.db_insert() frappe.enqueue( @@ -155,7 +160,7 @@ def queue_bom_cost_jobs( doc=update_doc, bom_list=boms_to_process, batch_name=batch_row.name, - timeout=40000, + queue="long", ) @@ -181,9 +186,11 @@ def resume_bom_cost_update_jobs(): for log in in_progress_logs: # check if all log batches of current level are processed bom_batches = frappe.db.get_all( - "BOM Update Batch", {"parent": log.name, "level": log.current_level}, ["name", "boms_updated"] + "BOM Update Batch", + {"parent": log.name, "level": log.current_level}, + ["name", "boms_updated", "status"], ) - incomplete_level = any(not row.get("boms_updated") for row in bom_batches) + incomplete_level = any(row.get("status") == "Pending" for row in bom_batches) if not bom_batches or incomplete_level: continue @@ -195,14 +202,15 @@ def resume_bom_cost_update_jobs(): log.name, values={ "processed_boms": json.dumps(processed_boms), - "parent_boms": json.dumps(parent_boms), "status": "Completed" if not parent_boms else "In Progress", }, commit=True, ) if parent_boms: # there is a next level to process - process_boms_cost_level_wise(update_doc=frappe.get_doc("BOM Update Log", log.name)) + process_boms_cost_level_wise( + update_doc=frappe.get_doc("BOM Update Log", log.name), parent_boms=parent_boms + ) def get_processed_current_boms( diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py index 2d6429b050..49e747c4bb 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py @@ -3,7 +3,7 @@ import json from collections import defaultdict -from typing import TYPE_CHECKING, Any, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union if TYPE_CHECKING: from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import BOMUpdateLog @@ -38,7 +38,9 @@ def replace_bom(boms: Dict) -> None: bom_obj.save_version() -def update_cost_in_level(doc: "BOMUpdateLog", bom_list: List[str], batch_name: int) -> None: +def update_cost_in_level( + doc: "BOMUpdateLog", bom_list: List[str], batch_name: Union[int, str] +) -> None: "Updates Cost for BOMs within a given level. Runs via background jobs." try: @@ -49,7 +51,14 @@ def update_cost_in_level(doc: "BOMUpdateLog", bom_list: List[str], batch_name: i frappe.db.auto_commit_on_many_writes = 1 update_cost_in_boms(bom_list=bom_list) # main updation logic - frappe.db.set_value("BOM Update Batch", batch_name, "boms_updated", json.dumps(bom_list)) + + bom_batch = frappe.qb.DocType("BOM Update Batch") + ( + frappe.qb.update(bom_batch) + .set(bom_batch.boms_updated, json.dumps(bom_list)) + .set(bom_batch.status, "Completed") + .where(bom_batch.name == batch_name) + ).run() except Exception: handle_exception(doc) finally: @@ -105,14 +114,17 @@ def get_bom_unit_cost(bom_name: str) -> float: def update_cost_in_boms(bom_list: List[str]) -> None: "Updates cost in given BOMs. Returns current and total updated BOMs." - for bom in bom_list: - bom_doc = frappe.get_cached_doc("BOM", bom) + for index, bom in enumerate(bom_list): + bom_doc = frappe.get_doc("BOM", bom, for_update=True) bom_doc.calculate_cost(save_updates=True, update_hour_rate=True) bom_doc.db_update() + if index % 100 == 0: + frappe.db.commit() + def get_next_higher_level_boms( - child_boms: Dict[str, bool], processed_boms: Dict[str, bool] + child_boms: List[str], processed_boms: Dict[str, bool] ) -> List[str]: "Generate immediate higher level dependants with no unresolved dependencies (children)." diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py index 758d8ed0ef..d16fcd0832 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -40,7 +40,7 @@ def auto_update_latest_price_in_all_boms() -> None: if frappe.db.get_single_value("Manufacturing Settings", "update_bom_costs_automatically"): wip_log = frappe.get_all( "BOM Update Log", - {"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress", "Paused"]]}, + {"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress"]]}, limit_page_length=1, ) if not wip_log: From 15101190a6efb4a596824a1c7cba88d8363e5d17 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 6 Jun 2022 17:12:43 +0530 Subject: [PATCH 158/192] chore: `get_valuation_rate` in bom.py must always return float & goto Item master if no bins --- erpnext/manufacturing/doctype/bom/bom.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 1fb00ef3fe..8bf124e058 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -961,11 +961,8 @@ def get_valuation_rate(args): valuation_rate = item_valuation.get("valuation_rate") - if valuation_rate is None: - # Explicit null value check. If null, Bins don't exist, neither does SLE - return valuation_rate - - if valuation_rate <= 0: + if (valuation_rate is not None) and valuation_rate <= 0: + # Explicit null value check. If None, Bins don't exist, neither does SLE last_valuation_rate = frappe.db.sql( """select valuation_rate from `tabStock Ledger Entry` From 91f9f37d64b54625888f2642edd1b11a8cde82a6 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 7 Jun 2022 09:51:30 +0530 Subject: [PATCH 159/192] fix: leave balance for earned leaves in backdated Leave Application dashboard (#31253) --- .../leave_application/leave_application.js | 2 +- .../leave_application/leave_application.py | 23 +-- .../test_leave_application.py | 139 ++++++++++++------ 3 files changed, 101 insertions(+), 63 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js index 85997a4087..ee00e6719c 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.js +++ b/erpnext/hr/doctype/leave_application/leave_application.js @@ -173,7 +173,7 @@ frappe.ui.form.on("Leave Application", { date: frm.doc.from_date, to_date: frm.doc.to_date, leave_type: frm.doc.leave_type, - consider_all_leaves_in_the_allocation_period: true + consider_all_leaves_in_the_allocation_period: 1 }, callback: function (r) { if (!r.exc && r.message) { diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 53c5df4210..43c2bb37b2 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -757,22 +757,6 @@ def get_leave_details(employee, date): leave_allocation = {} for d in allocation_records: allocation = allocation_records.get(d, frappe._dict()) - - total_allocated_leaves = ( - frappe.db.get_value( - "Leave Allocation", - { - "from_date": ("<=", date), - "to_date": (">=", date), - "employee": employee, - "leave_type": allocation.leave_type, - "docstatus": 1, - }, - "SUM(total_leaves_allocated)", - ) - or 0 - ) - remaining_leaves = get_leave_balance_on( employee, d, date, to_date=allocation.to_date, consider_all_leaves_in_the_allocation_period=True ) @@ -782,10 +766,11 @@ def get_leave_details(employee, date): leaves_pending = get_leaves_pending_approval_for_period( employee, d, allocation.from_date, end_date ) + expired_leaves = allocation.total_leaves_allocated - (remaining_leaves + leaves_taken) leave_allocation[d] = { - "total_leaves": total_allocated_leaves, - "expired_leaves": total_allocated_leaves - (remaining_leaves + leaves_taken), + "total_leaves": allocation.total_leaves_allocated, + "expired_leaves": expired_leaves if expired_leaves > 0 else 0, "leaves_taken": leaves_taken, "leaves_pending_approval": leaves_pending, "remaining_leaves": remaining_leaves, @@ -830,7 +815,7 @@ def get_leave_balance_on( allocation_records = get_leave_allocation_records(employee, date, leave_type) allocation = allocation_records.get(leave_type, frappe._dict()) - end_date = allocation.to_date if consider_all_leaves_in_the_allocation_period else date + end_date = allocation.to_date if cint(consider_all_leaves_in_the_allocation_period) else date cf_expiry = get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, date) leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date) diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index 7506c61108..27c54109de 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -76,7 +76,14 @@ _test_records = [ class TestLeaveApplication(unittest.TestCase): def setUp(self): - for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Leave Ledger Entry"]: + for dt in [ + "Leave Application", + "Leave Allocation", + "Salary Slip", + "Leave Ledger Entry", + "Leave Period", + "Leave Policy Assignment", + ]: frappe.db.delete(dt) frappe.set_user("Administrator") @@ -702,59 +709,24 @@ class TestLeaveApplication(unittest.TestCase): self.assertEqual(details.leave_balance, 30) def test_earned_leaves_creation(self): - - frappe.db.sql("""delete from `tabLeave Period`""") - frappe.db.sql("""delete from `tabLeave Policy Assignment`""") - frappe.db.sql("""delete from `tabLeave Allocation`""") - frappe.db.sql("""delete from `tabLeave Ledger Entry`""") + from erpnext.hr.utils import allocate_earned_leaves leave_period = get_leave_period() employee = get_employee() leave_type = "Test Earned Leave Type" - frappe.delete_doc_if_exists("Leave Type", "Test Earned Leave Type", force=1) - frappe.get_doc( - dict( - leave_type_name=leave_type, - doctype="Leave Type", - is_earned_leave=1, - earned_leave_frequency="Monthly", - rounding=0.5, - max_leaves_allowed=6, - ) - ).insert() + make_policy_assignment(employee, leave_type, leave_period) - leave_policy = frappe.get_doc( - { - "doctype": "Leave Policy", - "title": "Test Leave Policy", - "leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}], - } - ).insert() - - data = { - "assignment_based_on": "Leave Period", - "leave_policy": leave_policy.name, - "leave_period": leave_period.name, - } - - leave_policy_assignments = create_assignment_for_multiple_employees( - [employee.name], frappe._dict(data) - ) - - from erpnext.hr.utils import allocate_earned_leaves - - i = 0 - while i < 14: + for i in range(0, 14): allocate_earned_leaves() - i += 1 + self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6) # validate earned leaves creation without maximum leaves frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 0) - i = 0 - while i < 6: + + for i in range(0, 6): allocate_earned_leaves() - i += 1 + self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9) # test to not consider current leave in leave balance while submitting @@ -970,6 +942,54 @@ class TestLeaveApplication(unittest.TestCase): self.assertEqual(leave_allocation["leaves_pending_approval"], 1) self.assertEqual(leave_allocation["remaining_leaves"], 26) + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") + def test_get_earned_leave_details_for_dashboard(self): + from erpnext.hr.utils import allocate_earned_leaves + + leave_period = get_leave_period() + employee = get_employee() + leave_type = "Test Earned Leave Type" + leave_policy_assignments = make_policy_assignment(employee, leave_type, leave_period) + allocation = frappe.db.get_value( + "Leave Allocation", + {"leave_policy_assignment": leave_policy_assignments[0]}, + "name", + ) + allocation = frappe.get_doc("Leave Allocation", allocation) + allocation.new_leaves_allocated = 2 + allocation.save() + + for i in range(0, 6): + allocate_earned_leaves() + + first_sunday = get_first_sunday(self.holiday_list) + make_leave_application( + employee.name, add_days(first_sunday, 1), add_days(first_sunday, 1), leave_type + ) + + details = get_leave_details(employee.name, allocation.from_date) + leave_allocation = details["leave_allocation"][leave_type] + expected = { + "total_leaves": 2.0, + "expired_leaves": 0.0, + "leaves_taken": 1.0, + "leaves_pending_approval": 0.0, + "remaining_leaves": 1.0, + } + self.assertEqual(leave_allocation, expected) + + details = get_leave_details(employee.name, getdate()) + leave_allocation = details["leave_allocation"][leave_type] + + expected = { + "total_leaves": 5.0, + "expired_leaves": 0.0, + "leaves_taken": 1.0, + "leaves_pending_approval": 0.0, + "remaining_leaves": 4.0, + } + self.assertEqual(leave_allocation, expected) + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") def test_get_leave_allocation_records(self): employee = get_employee() @@ -1100,3 +1120,36 @@ def get_first_sunday(holiday_list, for_date=None): )[0][0] return first_sunday + + +def make_policy_assignment(employee, leave_type, leave_period): + frappe.delete_doc_if_exists("Leave Type", leave_type, force=1) + frappe.get_doc( + dict( + leave_type_name=leave_type, + doctype="Leave Type", + is_earned_leave=1, + earned_leave_frequency="Monthly", + rounding=0.5, + max_leaves_allowed=6, + ) + ).insert() + + leave_policy = frappe.get_doc( + { + "doctype": "Leave Policy", + "title": "Test Leave Policy", + "leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}], + } + ).insert() + + data = { + "assignment_based_on": "Leave Period", + "leave_policy": leave_policy.name, + "leave_period": leave_period.name, + } + + leave_policy_assignments = create_assignment_for_multiple_employees( + [employee.name], frappe._dict(data) + ) + return leave_policy_assignments From c3f2201c45fa2bdbffdd31ba506120cc73658bfd Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 7 Jun 2022 10:04:35 +0530 Subject: [PATCH 160/192] chore(meta): apply stale rules to pull only --- .github/stale.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/stale.yml b/.github/stale.yml index cdce0aee08..da15d32680 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -23,3 +23,5 @@ pulls: activity occurs, but it only takes a comment to keep a contribution alive :) Also, even if it is closed, you can always reopen the PR when you're ready. Thank you for contributing. + +only: pulls From dc8e80ea815d5684b56376330500f8dccdd38816 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 7 Jun 2022 11:35:03 +0530 Subject: [PATCH 161/192] test: Add test coverage for cancellation --- .../purchase_invoice/test_purchase_invoice.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 30d26acf3a..9b7b88973f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1526,6 +1526,18 @@ class TestPurchaseInvoice(unittest.TestCase): check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date) + # Cancel purchase invoice to check reverse provisional entry cancellation + pi.cancel() + + expected_gle_for_purchase_receipt_post_pi_cancel = [ + ["Provision Account - _TC", 0, 250, pi.posting_date], + ["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date], + ] + + check_gl_entries( + self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date + ) + company.enable_provisional_accounting_for_non_stock_items = 0 company.save() From 293eb8d722c773864eef6ef45ce36a8bda25340e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 10 May 2022 21:30:31 +0530 Subject: [PATCH 162/192] test: create stock test mixin for assertion/utils --- .../doctype/stock_entry/stock_entry_utils.py | 26 +++++++++++ .../test_stock_ledger_entry.py | 27 +----------- .../test_stock_reconciliation.py | 15 ++++--- erpnext/stock/tests/test_utils.py | 44 ++++++++++++++++--- 4 files changed, 75 insertions(+), 37 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py index c5c0cefe51..41a3b8916d 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py @@ -2,11 +2,37 @@ # See license.txt +from typing import TYPE_CHECKING, Optional, overload + import frappe from frappe.utils import cint, flt import erpnext +if TYPE_CHECKING: + from erpnext.stock.doctype.stock_entry.stock_entry import StockEntry + + +@overload +def make_stock_entry( + *, + item_code: str, + qty: float, + company: Optional[str] = None, + from_warehouse: Optional[str] = None, + to_warehouse: Optional[str] = None, + rate: Optional[float] = None, + serial_no: Optional[str] = None, + batch_no: Optional[str] = None, + posting_date: Optional[str] = None, + posting_time: Optional[str] = None, + purpose: Optional[str] = None, + do_not_save: bool = False, + do_not_submit: bool = False, + inspection_required: bool = False, +) -> "StockEntry": + ... + @frappe.whitelist() def make_stock_entry(**args): diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index eb1e0fc25f..55a213ccc3 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -24,9 +24,10 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation, ) from erpnext.stock.stock_ledger import get_previous_sle +from erpnext.stock.tests.test_utils import StockTestMixin -class TestStockLedgerEntry(FrappeTestCase): +class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): def setUp(self): items = create_items() reset("Stock Entry") @@ -541,30 +542,6 @@ class TestStockLedgerEntry(FrappeTestCase): "Incorrect 'Incoming Rate' values fetched for DN items", ) - def assertSLEs(self, doc, expected_sles, sle_filters=None): - """Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line""" - - filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0} - if sle_filters: - filters.update(sle_filters) - sles = frappe.get_all( - "Stock Ledger Entry", - fields=["*"], - filters=filters, - order_by="timestamp(posting_date, posting_time), creation", - ) - - for exp_sle, act_sle in zip(expected_sles, sles): - for k, v in exp_sle.items(): - act_value = act_sle[k] - if k == "stock_queue": - act_value = json.loads(act_value) - if act_value and act_value[0][0] == 0: - # ignore empty fifo bins - continue - - self.assertEqual(v, act_value, msg=f"{k} doesn't match \n{exp_sle}\n{act_sle}") - def test_batchwise_item_valuation_stock_reco(self): item, warehouses, batches = setup_item_valuation_test() state = {"stock_value": 0.0, "qty": 0.0} diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 9088eb802b..191c03f5f1 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -10,7 +10,7 @@ from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, cstr, flt, nowdate, nowtime, random_string from erpnext.accounts.utils import get_stock_and_account_balance -from erpnext.stock.doctype.item.test_item import create_item, make_item +from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( @@ -19,10 +19,11 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( ) from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after +from erpnext.stock.tests.test_utils import StockTestMixin from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method -class TestStockReconciliation(FrappeTestCase): +class TestStockReconciliation(FrappeTestCase, StockTestMixin): @classmethod def setUpClass(cls): create_batch_or_serial_no_items() @@ -40,7 +41,7 @@ class TestStockReconciliation(FrappeTestCase): self._test_reco_sle_gle("Moving Average") def _test_reco_sle_gle(self, valuation_method): - item_code = make_item(properties={"valuation_method": valuation_method}).name + item_code = self.make_item(properties={"valuation_method": valuation_method}).name se1, se2, se3 = insert_existing_sle(warehouse="Stores - TCP1", item_code=item_code) company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") @@ -392,7 +393,7 @@ class TestStockReconciliation(FrappeTestCase): SR4 | Reco | 0 | 6 (posting date: today-1) [backdated] PR3 | PR | 1 | 7 (posting date: today) # can't post future PR """ - item_code = make_item().name + item_code = self.make_item().name warehouse = "_Test Warehouse - _TC" frappe.flags.dont_execute_stock_reposts = True @@ -458,7 +459,7 @@ class TestStockReconciliation(FrappeTestCase): from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.stock_ledger import NegativeStockError - item_code = make_item().name + item_code = self.make_item().name warehouse = "_Test Warehouse - _TC" pr1 = make_purchase_receipt( @@ -506,7 +507,7 @@ class TestStockReconciliation(FrappeTestCase): from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.stock_ledger import NegativeStockError - item_code = make_item().name + item_code = self.make_item().name warehouse = "_Test Warehouse - _TC" sr = create_stock_reconciliation( @@ -549,7 +550,7 @@ class TestStockReconciliation(FrappeTestCase): # repost will make this test useless, qty should update in realtime without reposts frappe.flags.dont_execute_stock_reposts = True - item_code = make_item().name + item_code = self.make_item().name warehouse = "_Test Warehouse - _TC" sr = create_stock_reconciliation( diff --git a/erpnext/stock/tests/test_utils.py b/erpnext/stock/tests/test_utils.py index 9ee0c9f3b5..e07e08c79e 100644 --- a/erpnext/stock/tests/test_utils.py +++ b/erpnext/stock/tests/test_utils.py @@ -1,16 +1,50 @@ +import json + import frappe from frappe.tests.utils import FrappeTestCase -from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.utils import scan_barcode -class TestStockUtilities(FrappeTestCase): +class StockTestMixin: + """Mixin to simplfy stock ledger tests, useful for all stock transactions.""" + + def make_item(self, item_code=None, properties=None, *args, **kwargs): + from erpnext.stock.doctype.item.test_item import make_item + + return make_item(item_code, properties, *args, **kwargs) + + def assertSLEs(self, doc, expected_sles, sle_filters=None): + """Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line""" + + filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0} + if sle_filters: + filters.update(sle_filters) + sles = frappe.get_all( + "Stock Ledger Entry", + fields=["*"], + filters=filters, + order_by="timestamp(posting_date, posting_time), creation", + ) + + for exp_sle, act_sle in zip(expected_sles, sles): + for k, v in exp_sle.items(): + act_value = act_sle[k] + if k == "stock_queue": + act_value = json.loads(act_value) + if act_value and act_value[0][0] == 0: + # ignore empty fifo bins + continue + + self.assertEqual(v, act_value, msg=f"{k} doesn't match \n{exp_sle}\n{act_sle}") + + +class TestStockUtilities(FrappeTestCase, StockTestMixin): def test_barcode_scanning(self): - simple_item = make_item(properties={"barcodes": [{"barcode": "12399"}]}) + simple_item = self.make_item(properties={"barcodes": [{"barcode": "12399"}]}) self.assertEqual(scan_barcode("12399")["item_code"], simple_item.name) - batch_item = make_item(properties={"has_batch_no": 1, "create_new_batch": 1}) + batch_item = self.make_item(properties={"has_batch_no": 1, "create_new_batch": 1}) batch = frappe.get_doc(doctype="Batch", item=batch_item.name).insert() batch_scan = scan_barcode(batch.name) @@ -19,7 +53,7 @@ class TestStockUtilities(FrappeTestCase): self.assertEqual(batch_scan["has_batch_no"], 1) self.assertEqual(batch_scan["has_serial_no"], 0) - serial_item = make_item(properties={"has_serial_no": 1}) + serial_item = self.make_item(properties={"has_serial_no": 1}) serial = frappe.get_doc( doctype="Serial No", item_code=serial_item.name, serial_no=frappe.generate_hash() ).insert() From 7726271e2ac6776b29f795e6e54dd76aa6d581b8 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 1 Jun 2022 14:17:06 +0530 Subject: [PATCH 163/192] fix: purchase invoice return GLe voucher_wise_stock_value contains tuples and the condition was looking for string, so it's never triggered. Caused by https://github.com/frappe/erpnext/pull/24200 --- .../purchase_invoice/purchase_invoice.py | 2 +- .../purchase_invoice/test_purchase_invoice.py | 77 ++++++++++++++++++- .../controllers/sales_and_purchase_return.py | 2 +- erpnext/stock/tests/test_utils.py | 17 ++++ 4 files changed, 95 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index e6da6669ac..5d11dffdef 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1127,7 +1127,7 @@ class PurchaseInvoice(BuyingController): # Stock ledger value is not matching with the warehouse amount if ( self.update_stock - and voucher_wise_stock_value.get(item.name) + and voucher_wise_stock_value.get((item.name, item.warehouse)) and warehouse_debit_amount != flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision) ): diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 30d26acf3a..9c8a6dd6b2 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -27,12 +27,13 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import ( make_purchase_receipt, ) from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction +from erpnext.stock.tests.test_utils import StockTestMixin test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Template"] test_ignore = ["Serial No"] -class TestPurchaseInvoice(unittest.TestCase): +class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): @classmethod def setUpClass(self): unlink_payment_on_cancel_of_invoice() @@ -693,6 +694,80 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account][0], gle.debit) self.assertEqual(expected_values[gle.account][1], gle.credit) + def test_standalone_return_using_pi(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item = self.make_item().name + company = "_Test Company with perpetual inventory" + warehouse = "Stores - TCP1" + + make_stock_entry(item_code=item, target=warehouse, qty=50, rate=120) + + return_pi = make_purchase_invoice( + is_return=1, + item=item, + qty=-10, + update_stock=1, + rate=100, + company=company, + warehouse=warehouse, + cost_center="Main - TCP1", + ) + + # assert that stock consumption is with actual rate + self.assertGLEs( + return_pi, + [{"credit": 1200, "debit": 0}], + gle_filters={"account": "Stock In Hand - TCP1"}, + ) + + # assert loss booked in COGS + self.assertGLEs( + return_pi, + [{"credit": 0, "debit": 200}], + gle_filters={"account": "Cost of Goods Sold - TCP1"}, + ) + + def test_return_with_lcv(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import ( + create_landed_cost_voucher, + ) + + item = self.make_item().name + company = "_Test Company with perpetual inventory" + warehouse = "Stores - TCP1" + cost_center = "Main - TCP1" + + pi = make_purchase_invoice( + item=item, + company=company, + warehouse=warehouse, + cost_center=cost_center, + update_stock=1, + qty=10, + rate=100, + ) + + # Create landed cost voucher - will increase valuation of received item by 10 + create_landed_cost_voucher("Purchase Invoice", pi.name, pi.company, charges=100) + return_pi = make_return_doc(pi.doctype, pi.name) + return_pi.save().submit() + + # assert that stock consumption is with actual in rate + self.assertGLEs( + return_pi, + [{"credit": 1100, "debit": 0}], + gle_filters={"account": "Stock In Hand - TCP1"}, + ) + + # assert loss booked in COGS + self.assertGLEs( + return_pi, + [{"credit": 0, "debit": 100}], + gle_filters={"account": "Cost of Goods Sold - TCP1"}, + ) + def test_multi_currency_gle(self): pi = make_purchase_invoice( supplier="_Test Supplier USD", diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index bd4b59b385..d24ac3f2cf 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -316,7 +316,7 @@ def get_returned_qty_map_for_row(return_against, party, row_name, doctype): return data[0] -def make_return_doc(doctype, source_name, target_doc=None): +def make_return_doc(doctype: str, source_name: str, target_doc=None): from frappe.model.mapper import get_mapped_doc from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos diff --git a/erpnext/stock/tests/test_utils.py b/erpnext/stock/tests/test_utils.py index e07e08c79e..b046dbda24 100644 --- a/erpnext/stock/tests/test_utils.py +++ b/erpnext/stock/tests/test_utils.py @@ -38,6 +38,23 @@ class StockTestMixin: self.assertEqual(v, act_value, msg=f"{k} doesn't match \n{exp_sle}\n{act_sle}") + def assertGLEs(self, doc, expected_gles, gle_filters=None, order_by=None): + filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0} + + if gle_filters: + filters.update(gle_filters) + actual_gles = frappe.get_all( + "GL Entry", + fields=["*"], + filters=filters, + order_by=order_by or "posting_date, creation", + ) + + for exp_gle, act_gle in zip(expected_gles, actual_gles): + for k, exp_value in exp_gle.items(): + act_value = act_gle[k] + self.assertEqual(exp_value, act_value, msg=f"{k} doesn't match \n{exp_gle}\n{act_gle}") + class TestStockUtilities(FrappeTestCase, StockTestMixin): def test_barcode_scanning(self): From 815141bf57b3f7710993c0d0d871ea2457d0488f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 7 Jun 2022 13:16:06 +0530 Subject: [PATCH 164/192] fix: Close unsecured terms loans --- erpnext/loan_management/doctype/loan/loan.js | 18 ++++++++++++++++ erpnext/loan_management/doctype/loan/loan.py | 22 +++++++++++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js index 940a1bbc00..38328e6967 100644 --- a/erpnext/loan_management/doctype/loan/loan.js +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -93,6 +93,12 @@ frappe.ui.form.on('Loan', { frm.trigger("make_loan_refund"); },__('Create')); } + + if (frm.doc.status == "Loan Closure Requested" && frm.doc.is_term_loan && !frm.doc.is_secured_loan) { + frm.add_custom_button(__('Close Loan'), function() { + frm.trigger("close_unsecured_term_loan"); + },__('Status')); + } } frm.trigger("toggle_fields"); }, @@ -174,6 +180,18 @@ frappe.ui.form.on('Loan', { }) }, + close_unsecured_term_loan: function(frm) { + frappe.call({ + args: { + "loan": frm.doc.name + }, + method: "erpnext.loan_management.doctype.loan.loan.close_unsecured_term_loan", + callback: function () { + frm.refresh(); + } + }) + }, + request_loan_closure: function(frm) { frappe.confirm(__("Do you really want to close this loan"), function() { diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index 3b76ba4edb..ac8b3629d9 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -60,11 +60,11 @@ class Loan(AccountsController): ) def validate_cost_center(self): - if not self.cost_center and self.rate_of_interest != 0: + if not self.cost_center and self.rate_of_interest != 0.0: self.cost_center = frappe.db.get_value("Company", self.company, "cost_center") - if not self.cost_center: - frappe.throw(_("Cost center is mandatory for loans having rate of interest greater than 0")) + if not self.cost_center: + frappe.throw(_("Cost center is mandatory for loans having rate of interest greater than 0")) def on_submit(self): self.link_loan_security_pledge() @@ -342,6 +342,22 @@ def get_loan_application(loan_application): return loan.as_dict() +@frappe.whitelist() +def close_unsecured_term_loan(loan): + loan_details = frappe.db.get_value( + "Loan", {"name": loan}, ["status", "is_term_loan", "is_secured_loan"], as_dict=1 + ) + + if ( + loan_details.status == "Loan Closure Requested" + and loan_details.is_term_loan + and not loan_details.is_secured_loan + ): + frappe.db.set_value("Loan", loan, "status", "Closed") + else: + frappe.throw(_("Cannot close this loan until full repayment")) + + def close_loan(loan, total_amount_paid): frappe.db.set_value("Loan", loan, "total_amount_paid", total_amount_paid) frappe.db.set_value("Loan", loan, "status", "Closed") From 6bde1bb5d2446c3ed08f566060c321664ad1d4e4 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 7 Jun 2022 14:44:00 +0530 Subject: [PATCH 165/192] test: Util to update cost in all BOMs - Utility to update cost in all BOMs without cron jobs or background jobs (run immediately) - Re-use util wherever all bom costs are to be updated - Skip explicit commits if in test - Specify company in test records (dirty data sometimes, company wh mismatch) - Skip background jobs queueing if in test --- erpnext/manufacturing/doctype/bom/test_bom.py | 6 +- .../doctype/bom/test_records.json | 1 + .../doctype/bom_update_log/bom_update_log.py | 21 +++- .../bom_update_log/bom_updation_utils.py | 10 +- .../bom_update_log/test_bom_update_log.py | 97 ++++++++++++------- .../bom_update_tool/test_bom_update_tool.py | 14 +-- 6 files changed, 97 insertions(+), 52 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 62fc0724e0..bc1bea7389 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -11,7 +11,9 @@ from frappe.utils import cstr, flt from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.manufacturing.doctype.bom.bom import item_query, make_variant_bom -from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost +from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import ( + update_cost_in_all_boms_in_test, +) from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_stock_reconciliation, @@ -80,7 +82,7 @@ class TestBOM(FrappeTestCase): reset_item_valuation_rate(item_code="_Test Item 2", qty=200, rate=rm_rate + 10) # update cost of all BOMs based on latest valuation rate - update_cost() + update_cost_in_all_boms_in_test() # check if new valuation rate updated in all BOMs for d in frappe.db.sql( diff --git a/erpnext/manufacturing/doctype/bom/test_records.json b/erpnext/manufacturing/doctype/bom/test_records.json index 25730f9b9f..507d319b51 100644 --- a/erpnext/manufacturing/doctype/bom/test_records.json +++ b/erpnext/manufacturing/doctype/bom/test_records.json @@ -32,6 +32,7 @@ "is_active": 1, "is_default": 1, "item": "_Test Item Home Desktop Manufactured", + "company": "_Test Company", "quantity": 1.0 }, { diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index d714b9d5fd..71430bd57e 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -1,7 +1,7 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt import json -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple, Union import frappe from frappe import _ @@ -101,12 +101,14 @@ def run_replace_bom_job( handle_exception(doc) finally: frappe.db.auto_commit_on_many_writes = 0 - frappe.db.commit() # nosemgrep + + if not frappe.flags.in_test: + frappe.db.commit() # nosemgrep def process_boms_cost_level_wise( update_doc: "BOMUpdateLog", parent_boms: List[str] = None -) -> None: +) -> Union[None, Tuple]: "Queue jobs at the start of new BOM Level in 'Update Cost' Jobs." current_boms = {} @@ -133,6 +135,10 @@ def process_boms_cost_level_wise( values = {"current_level": current_level} set_values_in_log(update_doc.name, values, commit=True) + + if frappe.flags.in_test: + return current_boms, current_level + queue_bom_cost_jobs(current_boms, update_doc, current_level) @@ -155,6 +161,10 @@ def queue_bom_cost_jobs( ) batch_row.db_insert() + if frappe.flags.in_test: + # skip background jobs in test + return boms_to_process, batch_row.name + frappe.enqueue( method="erpnext.manufacturing.doctype.bom_update_log.bom_updation_utils.update_cost_in_level", doc=update_doc, @@ -216,7 +226,10 @@ def resume_bom_cost_update_jobs(): def get_processed_current_boms( log: Dict[str, Any], bom_batches: Dict[str, Any] ) -> Tuple[List[str], Dict[str, Any]]: - "Aggregate all BOMs from BOM Update Batch rows into 'processed_boms' field and into current boms list." + """ + Aggregate all BOMs from BOM Update Batch rows into 'processed_boms' field + and into current boms list. + """ processed_boms = json.loads(log.processed_boms) if log.processed_boms else {} current_boms = [] diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py index 49e747c4bb..dde1e4ed75 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py @@ -63,7 +63,9 @@ def update_cost_in_level( handle_exception(doc) finally: frappe.db.auto_commit_on_many_writes = 0 - frappe.db.commit() # nosemgrep + + if not frappe.flags.in_test: + frappe.db.commit() # nosemgrep def get_ancestor_boms(new_bom: str, bom_list: Optional[List] = None) -> List: @@ -119,8 +121,8 @@ def update_cost_in_boms(bom_list: List[str]) -> None: bom_doc.calculate_cost(save_updates=True, update_hour_rate=True) bom_doc.db_update() - if index % 100 == 0: - frappe.db.commit() + if (index % 100 == 0) and not frappe.flags.in_test: + frappe.db.commit() # nosemgrep def get_next_higher_level_boms( @@ -210,7 +212,7 @@ def set_values_in_log(log_name: str, values: Dict[str, Any], commit: bool = Fals query = query.set(key, value) query.run() - if commit: + if commit and not frappe.flags.in_test: frappe.db.commit() # nosemgrep diff --git a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py index 4f151334a2..d770f6c56a 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py @@ -1,14 +1,27 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt +import json + import frappe from frappe.tests.utils import FrappeTestCase from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import ( BOMMissingError, + get_processed_current_boms, + process_boms_cost_level_wise, + queue_bom_cost_jobs, run_replace_bom_job, ) -from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import enqueue_replace_bom +from erpnext.manufacturing.doctype.bom_update_log.bom_updation_utils import ( + get_next_higher_level_boms, + set_values_in_log, + update_cost_in_level, +) +from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import ( + enqueue_replace_bom, + enqueue_update_cost, +) test_records = frappe.get_test_records("BOM") @@ -31,17 +44,12 @@ class TestBOMUpdateLog(FrappeTestCase): def tearDown(self): frappe.db.rollback() - if self._testMethodName == "test_bom_update_log_completion": - # clear logs and delete BOM created via setUp - frappe.db.delete("BOM Update Log") - self.new_bom_doc.cancel() - self.new_bom_doc.delete() - - # explicitly commit and restore to original state - frappe.db.commit() # nosemgrep - def test_bom_update_log_validate(self): - "Test if BOM presence is validated." + """ + 1) Test if BOM presence is validated. + 2) Test if same BOMs are validated. + 3) Test of non-existent BOM is validated. + """ with self.assertRaises(BOMMissingError): enqueue_replace_bom(boms={}) @@ -55,9 +63,7 @@ class TestBOMUpdateLog(FrappeTestCase): def test_bom_update_log_queueing(self): "Test if BOM Update Log is created and queued." - log = enqueue_replace_bom( - boms=self.boms, - ) + log = enqueue_replace_bom(boms=self.boms) self.assertEqual(log.docstatus, 1) self.assertEqual(log.status, "Queued") @@ -65,32 +71,51 @@ class TestBOMUpdateLog(FrappeTestCase): def test_bom_update_log_completion(self): "Test if BOM Update Log handles job completion correctly." - log = enqueue_replace_bom( - boms=self.boms, - ) + log = enqueue_replace_bom(boms=self.boms) - # Explicitly commits log, new bom (setUp) and replacement impact. - # Is run via background jobs IRL - run_replace_bom_job( - doc=log, - boms=self.boms, - update_type="Replace BOM", - ) + # Is run via background job IRL + run_replace_bom_job(doc=log, boms=self.boms) log.reload() self.assertEqual(log.status, "Completed") - # teardown (undo replace impact) due to commit - boms = frappe._dict( - current_bom=self.boms.new_bom, - new_bom=self.boms.current_bom, + +def update_cost_in_all_boms_in_test(): + """ + Utility to run 'Update Cost' job in tests immediately without Cron job. + Run job for all levels (manually) until fully complete. + """ + parent_boms = [] + log = enqueue_update_cost() # create BOM Update Log + + while log.status != "Completed": + level_boms, current_level = process_boms_cost_level_wise(log, parent_boms) + log.reload() + + boms, batch = queue_bom_cost_jobs( + level_boms, log, current_level + ) # adds rows in log for tracking + log.reload() + + update_cost_in_level(log, boms, batch) # business logic + log.reload() + + # current level done, get next level boms + bom_batches = frappe.db.get_all( + "BOM Update Batch", + {"parent": log.name, "level": log.current_level}, + ["name", "boms_updated", "status"], ) - log2 = enqueue_replace_bom( - boms=self.boms, + current_boms, processed_boms = get_processed_current_boms(log, bom_batches) + parent_boms = get_next_higher_level_boms(child_boms=current_boms, processed_boms=processed_boms) + + set_values_in_log( + log.name, + values={ + "processed_boms": json.dumps(processed_boms), + "status": "Completed" if not parent_boms else "In Progress", + }, ) - run_replace_bom_job( # Explicitly commits - doc=log2, - boms=boms, - update_type="Replace BOM", - ) - self.assertEqual(log2.status, "Completed") + log.reload() + + return log diff --git a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py index fae72a0f6f..d1882e56e9 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py @@ -1,11 +1,13 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt import frappe from frappe.tests.utils import FrappeTestCase from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import replace_bom -from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost +from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import ( + update_cost_in_all_boms_in_test, +) from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.stock.doctype.item.test_item import create_item @@ -25,8 +27,8 @@ class TestBOMUpdateTool(FrappeTestCase): boms = frappe._dict(current_bom=current_bom, new_bom=bom_doc.name) replace_bom(boms) - self.assertFalse(frappe.db.sql("select name from `tabBOM Item` where bom_no=%s", current_bom)) - self.assertTrue(frappe.db.sql("select name from `tabBOM Item` where bom_no=%s", bom_doc.name)) + self.assertFalse(frappe.db.exists("BOM Item", {"bom_no": current_bom, "docstatus": 1})) + self.assertTrue(frappe.db.exists("BOM Item", {"bom_no": bom_doc.name, "docstatus": 1})) # reverse, as it affects other testcases boms.current_bom = bom_doc.name @@ -52,13 +54,13 @@ class TestBOMUpdateTool(FrappeTestCase): self.assertEqual(doc.total_cost, 200) frappe.db.set_value("Item", "BOM Cost Test Item 2", "valuation_rate", 200) - update_cost() + update_cost_in_all_boms_in_test() doc.load_from_db() self.assertEqual(doc.total_cost, 300) frappe.db.set_value("Item", "BOM Cost Test Item 2", "valuation_rate", 100) - update_cost() + update_cost_in_all_boms_in_test() doc.load_from_db() self.assertEqual(doc.total_cost, 200) From f830b57fd481d46973b6874387529394fe659539 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 7 Jun 2022 15:23:32 +0530 Subject: [PATCH 166/192] test: sales register report with conditions --- erpnext/accounts/report/sales_register/sales_register.py | 4 ++-- erpnext/accounts/test/test_reports.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py index 777d96ced1..33bd3c7496 100644 --- a/erpnext/accounts/report/sales_register/sales_register.py +++ b/erpnext/accounts/report/sales_register/sales_register.py @@ -367,8 +367,8 @@ def get_conditions(filters): if not filters.get(field) or field in accounting_dimensions_list: return "" return f""" and exists(select name from `tab{table}` - where parent=`tabSales Invoice`.name - and ifnull(`tab{table}`.{field}, '') = %({field})s)""" + where parent=`tabSales Invoice`.name + and ifnull(`tab{table}`.{field}, '') = %({field})s)""" conditions += get_sales_invoice_item_field_condition("mode_of_payments", "Sales Invoice Payment") conditions += get_sales_invoice_item_field_condition("cost_center") diff --git a/erpnext/accounts/test/test_reports.py b/erpnext/accounts/test/test_reports.py index 19fe74fffc..3f06c30adb 100644 --- a/erpnext/accounts/test/test_reports.py +++ b/erpnext/accounts/test/test_reports.py @@ -28,6 +28,7 @@ REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [ ("Item-wise Sales Register", {}), ("Item-wise Purchase Register", {}), ("Sales Register", {}), + ("Sales Register", {"item_group": "All Item Groups"}), ("Purchase Register", {}), ( "Tax Detail", From fb4f8d870be85037df1fd416be8fb93c1a85231f Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 8 Jun 2022 09:36:33 +0530 Subject: [PATCH 167/192] fix(india): e-invoice eligibility if company gstin is not configured (#31247) --- erpnext/regional/india/e_invoice/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 9add09beaf..5eb14a5ddd 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -55,6 +55,9 @@ def validate_eligibility(doc): return False invalid_company = not frappe.db.get_value("E Invoice User", {"company": doc.get("company")}) + invalid_company_gstin = not frappe.db.get_value( + "E Invoice User", {"gstin": doc.get("company_gstin")} + ) invalid_supply_type = doc.get("gst_category") not in [ "Registered Regular", "Registered Composition", @@ -71,6 +74,7 @@ def validate_eligibility(doc): if ( invalid_company + or invalid_company_gstin or invalid_supply_type or company_transaction or no_taxes_applied From 9f2793ccf1e185b28c0f6b480a304641923dbc30 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 7 Jun 2022 17:44:39 +0530 Subject: [PATCH 168/192] test: Fix `test_update_bom_cost_in_all_boms` - Use base_rate for assertions as rate is subject to change due to conversion factor (USD) --- erpnext/manufacturing/doctype/bom/test_bom.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index f2731ec5ef..04e937a7e3 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -71,26 +71,32 @@ class TestBOM(FrappeTestCase): def test_update_bom_cost_in_all_boms(self): # get current rate for '_Test Item 2' - rm_rate = frappe.db.sql( - """select rate from `tabBOM Item` - where parent='BOM-_Test Item Home Desktop Manufactured-001' - and item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'""" + bom_rates = frappe.db.get_values( + "BOM Item", + { + "parent": "BOM-_Test Item Home Desktop Manufactured-001", + "item_code": "_Test Item 2", + "docstatus": 1, + }, + fieldname=["rate", "base_rate"], + as_dict=True, ) - rm_rate = rm_rate[0][0] if rm_rate else 0 + rm_base_rate = bom_rates[0].get("base_rate") if bom_rates else 0 + rm_rate = bom_rates[0].get("rate") if bom_rates else 0 # Reset item valuation rate - reset_item_valuation_rate(item_code="_Test Item 2", qty=200, rate=rm_rate + 10) + reset_item_valuation_rate(item_code="_Test Item 2", qty=200, rate=rm_base_rate + 10) # update cost of all BOMs based on latest valuation rate update_cost_in_all_boms_in_test() # check if new valuation rate updated in all BOMs for d in frappe.db.sql( - """select rate from `tabBOM Item` + """select base_rate from `tabBOM Item` where item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'""", as_dict=1, ): - self.assertEqual(d.rate, rm_rate + 10) + self.assertEqual(d.base_rate, rm_base_rate + 10) def test_bom_cost(self): bom = frappe.copy_doc(test_records[2]) From 018bc2af43160c4b25446bdba63c9fc5412b0be2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 8 Jun 2022 06:12:40 +0530 Subject: [PATCH 169/192] fix: ignore payment ledger on cancellation of loan --- erpnext/loan_management/doctype/loan/loan.py | 2 +- .../doctype/loan_disbursement/loan_disbursement.py | 2 +- .../doctype/loan_interest_accrual/loan_interest_accrual.py | 2 +- .../loan_management/doctype/loan_repayment/loan_repayment.py | 2 +- .../loan_management/doctype/loan_write_off/loan_write_off.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index a0ef1b971c..bb32c946f2 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -71,7 +71,7 @@ class Loan(AccountsController): def on_cancel(self): self.unlink_loan_security_pledge() - self.ignore_linked_doctypes = ["GL Entry"] + self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"] def set_missing_fields(self): if not self.company: diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index 10174e531a..0c2042ba50 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -29,7 +29,7 @@ class LoanDisbursement(AccountsController): def on_cancel(self): self.set_status_and_amounts(cancel=1) self.make_gl_entries(cancel=1) - self.ignore_linked_doctypes = ["GL Entry"] + self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"] def set_missing_values(self): if not self.disbursement_date: diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index 3a4c6513e4..0aeb448918 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -32,7 +32,7 @@ class LoanInterestAccrual(AccountsController): self.update_is_accrued() self.make_gl_entries(cancel=1) - self.ignore_linked_doctypes = ["GL Entry"] + self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"] def update_is_accrued(self): frappe.db.set_value("Repayment Schedule", self.repayment_schedule_name, "is_accrued", 0) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 8614fcb9cd..35fbe3a38e 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -41,7 +41,7 @@ class LoanRepayment(AccountsController): self.check_future_accruals() self.update_repayment_schedule(cancel=1) self.mark_as_unpaid() - self.ignore_linked_doctypes = ["GL Entry"] + self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"] self.make_gl_entries(cancel=1) def set_missing_values(self, amounts): diff --git a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py index e19fd15fc8..25aecf673b 100644 --- a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py +++ b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py @@ -42,7 +42,7 @@ class LoanWriteOff(AccountsController): def on_cancel(self): self.update_outstanding_amount(cancel=1) - self.ignore_linked_doctypes = ["GL Entry"] + self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"] self.make_gl_entries(cancel=1) def update_outstanding_amount(self, cancel=0): From 7e41d84a116f2acd03984c98ec4eaa8e50ddc1d3 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 8 Jun 2022 14:01:04 +0530 Subject: [PATCH 170/192] chore: `get_valuation_rate` sider fixes - Use qb instead of db.sql - Don't use `args` as argument for function - Cleaner variable names --- erpnext/manufacturing/doctype/bom/bom.py | 48 ++++++++++--------- erpnext/manufacturing/doctype/bom/test_bom.py | 1 - 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 3e2a2d13f1..631548b309 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -932,44 +932,46 @@ def get_bom_item_rate(args, bom_doc): return flt(rate) -def get_valuation_rate(args): +def get_valuation_rate(data): """ 1) Get average valuation rate from all warehouses 2) If no value, get last valuation rate from SLE 3) If no value, get valuation rate from Item """ + from frappe.query_builder.functions import Sum + item_code, company = data.get("item_code"), data.get("company") valuation_rate = 0.0 - item_valuation = frappe.db.sql( - """ - select - (sum(bin.stock_value) / sum(bin.actual_qty)) as valuation_rate - from - `tabBin` bin, `tabWarehouse` warehouse - where - bin.item_code=%(item)s - and bin.warehouse = warehouse.name - and warehouse.company=%(company)s""", - {"item": args["item_code"], "company": args["company"]}, - as_dict=1, - )[0] + + bin_table = frappe.qb.DocType("Bin") + wh_table = frappe.qb.DocType("Warehouse") + item_valuation = ( + frappe.qb.from_(bin_table) + .join(wh_table) + .on(bin_table.warehouse == wh_table.name) + .select((Sum(bin_table.stock_value) / Sum(bin_table.actual_qty)).as_("valuation_rate")) + .where((bin_table.item_code == item_code) & (wh_table.company == company)) + ).run(as_dict=True)[0] valuation_rate = item_valuation.get("valuation_rate") if (valuation_rate is not None) and valuation_rate <= 0: # Explicit null value check. If None, Bins don't exist, neither does SLE - last_valuation_rate = frappe.db.sql( - """select valuation_rate - from `tabStock Ledger Entry` - where item_code = %s and valuation_rate > 0 and is_cancelled = 0 - order by posting_date desc, posting_time desc, creation desc limit 1""", - args["item_code"], - ) + sle = frappe.qb.DocType("Stock Ledger Entry") + last_val_rate = ( + frappe.qb.from_(sle) + .select(sle.valuation_rate) + .where((sle.item_code == item_code) & (sle.valuation_rate > 0) & (sle.is_cancelled == 0)) + .orderby(sle.posting_date, order=frappe.qb.desc) + .orderby(sle.posting_time, order=frappe.qb.desc) + .orderby(sle.creation, order=frappe.qb.desc) + .limit(1) + ).run(as_dict=True) - valuation_rate = flt(last_valuation_rate[0][0]) if last_valuation_rate else 0 + valuation_rate = flt(last_val_rate[0].get("valuation_rate")) if last_val_rate else 0 if not valuation_rate: - valuation_rate = frappe.db.get_value("Item", args["item_code"], "valuation_rate") + valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate") return flt(valuation_rate) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 04e937a7e3..182a20c6bb 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -82,7 +82,6 @@ class TestBOM(FrappeTestCase): as_dict=True, ) rm_base_rate = bom_rates[0].get("base_rate") if bom_rates else 0 - rm_rate = bom_rates[0].get("rate") if bom_rates else 0 # Reset item valuation rate reset_item_valuation_rate(item_code="_Test Item 2", qty=200, rate=rm_base_rate + 10) From 2832731601920b07c7083a20c49868e866640add Mon Sep 17 00:00:00 2001 From: Marica Date: Wed, 8 Jun 2022 15:52:13 +0530 Subject: [PATCH 171/192] fix: Use `frappe.as_unicode` to decode output of redis module list (#31282) - As of redis 7, a list is added to the result of fetching the module list - This list cannot be "decoded",so use `frappe.as_unicode` that handles bytes as well as other types --- erpnext/e_commerce/redisearch_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/e_commerce/redisearch_utils.py b/erpnext/e_commerce/redisearch_utils.py index 61b4b9ee1f..1f649c7b48 100644 --- a/erpnext/e_commerce/redisearch_utils.py +++ b/erpnext/e_commerce/redisearch_utils.py @@ -38,7 +38,7 @@ def is_search_module_loaded(): out = cache.execute_command("MODULE LIST") parsed_output = " ".join( - (" ".join([s.decode() for s in o if not isinstance(s, int)]) for o in out) + (" ".join([frappe.as_unicode(s) for s in o if not isinstance(s, int)]) for o in out) ) return "search" in parsed_output except Exception: From 5c6937865cf8e54b52012f3dbefdb536f07586d7 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Wed, 8 Jun 2022 16:30:44 +0530 Subject: [PATCH 172/192] fix(ux): Add tabs in Item --- .../module_onboarding/accounts/accounts.json | 2 +- .../chart_of_accounts/chart_of_accounts.json | 6 +- .../setup_taxes/setup_taxes.json | 4 +- .../setup/module_onboarding/home/home.json | 2 +- .../data_import/data_import.json | 4 +- .../navigation_help/navigation_help.json | 2 +- erpnext/stock/doctype/item/item.json | 130 ++++++++---------- 7 files changed, 70 insertions(+), 80 deletions(-) diff --git a/erpnext/accounts/module_onboarding/accounts/accounts.json b/erpnext/accounts/module_onboarding/accounts/accounts.json index aa7cdf788b..b9040e3309 100644 --- a/erpnext/accounts/module_onboarding/accounts/accounts.json +++ b/erpnext/accounts/module_onboarding/accounts/accounts.json @@ -13,7 +13,7 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/accounts", "idx": 0, "is_complete": 0, - "modified": "2022-01-18 18:35:52.326688", + "modified": "2022-06-07 14:29:21.352132", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts", diff --git a/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json b/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json index 67553baec7..0973ab3962 100644 --- a/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json +++ b/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json @@ -1,8 +1,8 @@ { - "action": "Watch Video", + "action": "Go to Page", "action_label": "Learn more about Chart of Accounts", "callback_message": "You can continue with the onboarding after exploring this page", - "callback_title": "Awesome Work", + "callback_title": "Explore Chart of Accounts", "creation": "2020-05-13 19:58:20.928127", "description": "# Chart Of Accounts\n\nERPNext sets up a simple chart of accounts for each Company you create, but you can modify it according to business and legal requirements.", "docstatus": 0, @@ -12,7 +12,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-08-13 11:46:25.878506", + "modified": "2022-06-07 14:21:26.264769", "modified_by": "Administrator", "name": "Chart of Accounts", "owner": "Administrator", diff --git a/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json b/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json index 9f4c873e34..b6e9f5cd87 100644 --- a/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json +++ b/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json @@ -2,14 +2,14 @@ "action": "Create Entry", "action_label": "Manage Sales Tax Templates", "creation": "2020-05-13 19:29:43.844463", - "description": "# Setting up Taxes\n\nERPNext lets you configure your taxes so that they are automatically applied in your buying and selling transactions. You can configure them globally or even on Items. ERPNext taxes are pre-configured for most regions.\n", + "description": "# Setting up Taxes\n\nERPNext lets you configure your taxes so that they are automatically applied in your buying and selling transactions. You can configure them globally or even on Items. ERPNext taxes are pre-configured for most regions.\n\n[Checkout pre-configured taxes](/app/sales-taxes-and-charges-template)\n", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-08-13 11:48:37.238610", + "modified": "2022-06-07 14:27:15.906286", "modified_by": "Administrator", "name": "Setup Taxes", "owner": "Administrator", diff --git a/erpnext/setup/module_onboarding/home/home.json b/erpnext/setup/module_onboarding/home/home.json index 1b2dbc6fea..f02fc454c0 100644 --- a/erpnext/setup/module_onboarding/home/home.json +++ b/erpnext/setup/module_onboarding/home/home.json @@ -25,7 +25,7 @@ "documentation_url": "https://docs.erpnext.com/docs/v13/user/manual/en/setting-up/company-setup", "idx": 0, "is_complete": 0, - "modified": "2021-12-15 14:23:52.460913", + "modified": "2022-06-07 14:31:00.575193", "modified_by": "Administrator", "module": "Setup", "name": "Home", diff --git a/erpnext/setup/onboarding_step/data_import/data_import.json b/erpnext/setup/onboarding_step/data_import/data_import.json index 48741dca01..4999a368d3 100644 --- a/erpnext/setup/onboarding_step/data_import/data_import.json +++ b/erpnext/setup/onboarding_step/data_import/data_import.json @@ -2,14 +2,14 @@ "action": "Watch Video", "action_label": "Learn more about data migration", "creation": "2021-05-19 05:29:16.809610", - "description": "# Import Data from Spreadsheet\n\nIn ERPNext, you can easily migrate your historical data using spreadsheets. You can use it for migrating not just masters (like Customer, Supplier, Items), but also for transactions like (outstanding invoices, opening stock and accounting entries, etc). If you are migrating from [Tally](https://tallysolutions.com/) or [Quickbooks](https://quickbooks.intuit.com/in/), we got special migration tools for you.", + "description": "# Import Data from Spreadsheet\n\nIn ERPNext, you can easily migrate your historical data using spreadsheets. You can use it for migrating not just masters (like Customer, Supplier, Items), but also for transactions like (outstanding invoices, opening stock and accounting entries, etc).", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-12-15 13:10:57.346422", + "modified": "2022-06-07 14:28:51.390813", "modified_by": "Administrator", "name": "Data import", "owner": "Administrator", diff --git a/erpnext/setup/onboarding_step/navigation_help/navigation_help.json b/erpnext/setup/onboarding_step/navigation_help/navigation_help.json index 388853df79..cf07968bc7 100644 --- a/erpnext/setup/onboarding_step/navigation_help/navigation_help.json +++ b/erpnext/setup/onboarding_step/navigation_help/navigation_help.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-12-15 14:20:55.441678", + "modified": "2022-06-07 14:28:00.901082", "modified_by": "Administrator", "name": "Navigation Help", "owner": "Administrator", diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 4f3e842995..2f6d4fb783 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -11,7 +11,7 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "name_and_description_section", + "details", "naming_series", "item_code", "variant_of", @@ -35,11 +35,11 @@ "over_billing_allowance", "image", "section_break_11", - "brand", "description", - "sb_barcodes", - "barcodes", + "brand", + "dashboard_tab", "inventory_section", + "inventory_settings_section", "shelf_life_in_days", "end_of_life", "default_material_request_type", @@ -49,6 +49,8 @@ "weight_per_unit", "weight_uom", "allow_negative_stock", + "sb_barcodes", + "barcodes", "reorder_section", "reorder_levels", "unit_of_measure_conversion", @@ -67,13 +69,13 @@ "has_variants", "variant_based_on", "attributes", - "defaults", + "accounting", "item_defaults", - "purchase_details", - "is_purchase_item", + "purchasing_tab", "purchase_uom", "min_order_qty", "safety_stock", + "is_purchase_item", "purchase_details_cb", "lead_time_days", "last_purchase_rate", @@ -83,33 +85,31 @@ "delivered_by_supplier", "column_break2", "supplier_items", + "deferred_expense_section", + "enable_deferred_expense", + "deferred_expense_account", + "no_of_months_exp", "foreign_trade_details", "country_of_origin", "column_break_59", "customs_tariff_number", "sales_details", "sales_uom", - "is_sales_item", "grant_commission", + "is_sales_item", "column_break3", "max_discount", "deferred_revenue", - "deferred_revenue_account", "enable_deferred_revenue", - "column_break_85", + "deferred_revenue_account", "no_of_months", - "deferred_expense_section", - "deferred_expense_account", - "enable_deferred_expense", - "column_break_88", - "no_of_months_exp", "customer_details", "customer_items", "item_tax_section_break", "taxes", - "inspection_criteria", - "quality_inspection_template", + "quality_tab", "inspection_required_before_purchase", + "quality_inspection_template", "inspection_required_before_delivery", "manufacturing", "default_bom", @@ -118,17 +118,10 @@ "customer_code", "default_item_manufacturer", "default_manufacturer_part_no", - "more_information_section", "published_in_website", "total_projected_qty" ], "fields": [ - { - "fieldname": "name_and_description_section", - "fieldtype": "Section Break", - "oldfieldtype": "Section Break", - "options": "fa fa-flag" - }, { "fieldname": "naming_series", "fieldtype": "Select", @@ -315,7 +308,7 @@ "collapsible_depends_on": "is_stock_item", "depends_on": "is_stock_item", "fieldname": "inventory_section", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Inventory", "oldfieldtype": "Section Break", "options": "fa fa-truck" @@ -514,31 +507,17 @@ "label": "Attributes", "options": "Item Variant Attribute" }, - { - "depends_on": "eval:!doc.is_fixed_asset", - "fieldname": "defaults", - "fieldtype": "Section Break", - "label": "Sales, Purchase, Accounting Defaults" - }, { "fieldname": "item_defaults", "fieldtype": "Table", "label": "Item Defaults", "options": "Item Default" }, - { - "collapsible": 1, - "fieldname": "purchase_details", - "fieldtype": "Section Break", - "label": "Purchase, Replenishment Details", - "oldfieldtype": "Section Break", - "options": "fa fa-shopping-cart" - }, { "default": "1", "fieldname": "is_purchase_item", "fieldtype": "Check", - "label": "Is Purchase Item" + "label": "Allow Purchase" }, { "fieldname": "purchase_uom", @@ -646,8 +625,8 @@ { "collapsible": 1, "fieldname": "sales_details", - "fieldtype": "Section Break", - "label": "Sales Details", + "fieldtype": "Tab Break", + "label": "Sales", "oldfieldtype": "Section Break", "options": "fa fa-tag" }, @@ -661,7 +640,7 @@ "default": "1", "fieldname": "is_sales_item", "fieldtype": "Check", - "label": "Is Sales Item" + "label": "Allow Sales" }, { "fieldname": "column_break3", @@ -696,10 +675,6 @@ "fieldtype": "Check", "label": "Enable Deferred Revenue" }, - { - "fieldname": "column_break_85", - "fieldtype": "Column Break" - }, { "depends_on": "enable_deferred_revenue", "fieldname": "no_of_months", @@ -726,10 +701,6 @@ "fieldtype": "Check", "label": "Enable Deferred Expense" }, - { - "fieldname": "column_break_88", - "fieldtype": "Column Break" - }, { "depends_on": "enable_deferred_expense", "fieldname": "no_of_months_exp", @@ -753,8 +724,8 @@ "collapsible": 1, "collapsible_depends_on": "taxes", "fieldname": "item_tax_section_break", - "fieldtype": "Section Break", - "label": "Item Tax", + "fieldtype": "Tab Break", + "label": "Tax", "oldfieldtype": "Section Break", "options": "fa fa-money" }, @@ -767,15 +738,6 @@ "oldfieldtype": "Table", "options": "Item Tax" }, - { - "collapsible": 1, - "depends_on": "eval:!doc.is_fixed_asset", - "fieldname": "inspection_criteria", - "fieldtype": "Section Break", - "label": "Inspection Criteria", - "oldfieldtype": "Section Break", - "options": "fa fa-search" - }, { "default": "0", "fieldname": "inspection_required_before_purchase", @@ -801,7 +763,7 @@ "collapsible": 1, "depends_on": "is_stock_item", "fieldname": "manufacturing", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Manufacturing", "oldfieldtype": "Section Break", "options": "fa fa-cogs" @@ -880,12 +842,6 @@ "label": "Default Manufacturer Part No", "read_only": 1 }, - { - "collapsible": 1, - "fieldname": "more_information_section", - "fieldtype": "Section Break", - "label": "More Information" - }, { "default": "0", "depends_on": "published_in_website", @@ -912,6 +868,40 @@ "fieldname": "allow_negative_stock", "fieldtype": "Check", "label": "Allow Negative Stock" + }, + { + "fieldname": "inventory_settings_section", + "fieldtype": "Section Break", + "label": "Inventory Settings" + }, + { + "fieldname": "purchasing_tab", + "fieldtype": "Tab Break", + "label": "Purchasing" + }, + { + "fieldname": "quality_tab", + "fieldtype": "Tab Break", + "label": "Quality" + }, + { + "fieldname": "details", + "fieldtype": "Tab Break", + "label": "Details", + "oldfieldtype": "Section Break", + "options": "fa fa-flag" + }, + { + "fieldname": "dashboard_tab", + "fieldtype": "Tab Break", + "label": "Dashboard", + "show_dashboard": 1 + }, + { + "depends_on": "eval:!doc.is_fixed_asset", + "fieldname": "accounting", + "fieldtype": "Tab Break", + "label": "Accounting" } ], "icon": "fa fa-tag", @@ -919,7 +909,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "modified": "2022-04-28 04:52:10.272256", + "modified": "2022-06-08 11:35:20.094546", "modified_by": "Administrator", "module": "Stock", "name": "Item", From 67c26325eecb63a81f2839df1287ce8358340d1f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 4 Jun 2022 14:46:35 +0530 Subject: [PATCH 173/192] fix: unnecessary GLE reposts In Sales/Purchase invoices credit/debit are flipped and negated while making GLE, this is unflipped while posting them but if we compare the flipped ones it will always result in comparison failure and repost it. --- erpnext/accounts/utils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 1869cc7b29..df5e37d83d 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1124,6 +1124,9 @@ def update_gl_entries_after( def repost_gle_for_stock_vouchers( stock_vouchers, posting_date, company=None, warehouse_account=None ): + + from erpnext.accounts.general_ledger import toggle_debit_credit_if_negative + if not stock_vouchers: return @@ -1144,8 +1147,10 @@ def repost_gle_for_stock_vouchers( gle = get_voucherwise_gl_entries(stock_vouchers, posting_date) for voucher_type, voucher_no in stock_vouchers: existing_gle = gle.get((voucher_type, voucher_no), []) - voucher_obj = frappe.get_cached_doc(voucher_type, voucher_no) - expected_gle = voucher_obj.get_gl_entries(warehouse_account) + voucher_obj = frappe.get_doc(voucher_type, voucher_no) + # Some transactions post credit as negative debit, this is handled while posting GLE + # but while comparing we need to make sure it's flipped so comparisons are accurate + expected_gle = toggle_debit_credit_if_negative(voucher_obj.get_gl_entries(warehouse_account)) if expected_gle: if not existing_gle or not compare_existing_and_expected_gle( existing_gle, expected_gle, precision From eb53a9727d2e465f74597d987f3f82b9f0530d7c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 4 Jun 2022 18:19:44 +0530 Subject: [PATCH 174/192] perf: commit GL reposting periodically If you have a huge list of docs to repost then maintaining transaction throughtout entire GL reposting is not only unnecessary but also creates performance issues. Periodically commiting the changes prevents lost progress and reduces memory usage. --- erpnext/accounts/utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index df5e37d83d..8711395d55 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1145,7 +1145,7 @@ def repost_gle_for_stock_vouchers( precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit")) or 2 gle = get_voucherwise_gl_entries(stock_vouchers, posting_date) - for voucher_type, voucher_no in stock_vouchers: + for idx, (voucher_type, voucher_no) in enumerate(stock_vouchers): existing_gle = gle.get((voucher_type, voucher_no), []) voucher_obj = frappe.get_doc(voucher_type, voucher_no) # Some transactions post credit as negative debit, this is handled while posting GLE @@ -1160,6 +1160,11 @@ def repost_gle_for_stock_vouchers( else: _delete_gl_entries(voucher_type, voucher_no) + if idx % 20 == 0: + # Commit every 20 documents to avoid losing progress + # and reducing memory usage + frappe.db.commit() + def sort_stock_vouchers_by_posting_date( stock_vouchers: List[Tuple[str, str]] From 20f568c159424a728d8cf87b087efea069732579 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 9 Jun 2022 11:50:37 +0530 Subject: [PATCH 175/192] fix(India): Incorrect taxable in GSTR-3B report --- .../doctype/gstr_3b_report/gstr_3b_report.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py index 91fccfa6e8..77b8a3fecd 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -244,11 +244,10 @@ class GSTR3BReport(Document): ) for d in item_details: - if d.item_code not in self.invoice_items.get(d.parent, {}): - self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0) - self.invoice_items[d.parent][d.item_code] += d.get("taxable_value", 0) or d.get( - "base_net_amount", 0 - ) + self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0) + self.invoice_items[d.parent][d.item_code] += d.get("taxable_value", 0) or d.get( + "base_net_amount", 0 + ) if d.is_nil_exempt and d.item_code not in self.is_nil_exempt: self.is_nil_exempt.append(d.item_code) @@ -335,7 +334,7 @@ class GSTR3BReport(Document): def set_outward_taxable_supplies(self): inter_state_supply_details = {} - + invoice_list = {} for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): gst_category = self.invoice_detail_map.get(inv, {}).get("gst_category") place_of_supply = ( @@ -343,6 +342,8 @@ class GSTR3BReport(Document): ) export_type = self.invoice_detail_map.get(inv, {}).get("export_type") + invoice_list.setdefault(inv, 0.0) + for rate, items in items_based_on_rate.items(): for item_code, taxable_value in self.invoice_items.get(inv).items(): if item_code in items: @@ -361,7 +362,6 @@ class GSTR3BReport(Document): else: self.report_dict["sup_details"]["osup_det"]["iamt"] += taxable_value * rate / 100 self.report_dict["sup_details"]["osup_det"]["txval"] += taxable_value - if ( gst_category in ["Unregistered", "Registered Composition", "UIN Holders"] and self.gst_details.get("gst_state") != place_of_supply.split("-")[1] @@ -374,10 +374,12 @@ class GSTR3BReport(Document): inter_state_supply_details[(gst_category, place_of_supply)]["iamt"] += ( taxable_value * rate / 100 ) + invoice_list[inv] += taxable_value if self.invoice_cess.get(inv): self.report_dict["sup_details"]["osup_det"]["csamt"] += flt(self.invoice_cess.get(inv), 2) + print({k: v for k, v in sorted(invoice_list.items(), key=lambda item: item[1])}) self.set_inter_state_supply(inter_state_supply_details) def set_supplies_liable_to_reverse_charge(self): From 50aafdbe99ff3f35b6816863768d0144c1e17e7b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 9 Jun 2022 11:52:46 +0530 Subject: [PATCH 176/192] chore: cleanup --- erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py index 77b8a3fecd..090697b010 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -334,7 +334,6 @@ class GSTR3BReport(Document): def set_outward_taxable_supplies(self): inter_state_supply_details = {} - invoice_list = {} for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): gst_category = self.invoice_detail_map.get(inv, {}).get("gst_category") place_of_supply = ( @@ -342,8 +341,6 @@ class GSTR3BReport(Document): ) export_type = self.invoice_detail_map.get(inv, {}).get("export_type") - invoice_list.setdefault(inv, 0.0) - for rate, items in items_based_on_rate.items(): for item_code, taxable_value in self.invoice_items.get(inv).items(): if item_code in items: @@ -374,12 +371,10 @@ class GSTR3BReport(Document): inter_state_supply_details[(gst_category, place_of_supply)]["iamt"] += ( taxable_value * rate / 100 ) - invoice_list[inv] += taxable_value if self.invoice_cess.get(inv): self.report_dict["sup_details"]["osup_det"]["csamt"] += flt(self.invoice_cess.get(inv), 2) - print({k: v for k, v in sorted(invoice_list.items(), key=lambda item: item[1])}) self.set_inter_state_supply(inter_state_supply_details) def set_supplies_liable_to_reverse_charge(self): From bbaa14af1615cedc88b9e5f4d19289c6be6510fe Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 9 Jun 2022 15:14:44 +0530 Subject: [PATCH 177/192] fix: misaligned columns in print format of AR/AP report --- .../report/accounts_receivable/accounts_receivable.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html index f4fd06ba03..f2bf9424f7 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html @@ -42,7 +42,7 @@ {% if(filters.show_future_payments) { %} {% var balance_row = data.slice(-1).pop(); - var start = filters.based_on_payment_terms ? 13 : 11; + var start = report.columns.findIndex((elem) => (elem.fieldname == 'age')); var range1 = report.columns[start].label; var range2 = report.columns[start+1].label; var range3 = report.columns[start+2].label; From d9a52139523cf095d3cc60cf61483a8d56468595 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 9 Jun 2022 15:33:18 +0530 Subject: [PATCH 178/192] fix(ux): hide new version btn on unsaved BOM (#31297) --- erpnext/manufacturing/doctype/bom/bom.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index d74379881c..ecad41fe7b 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -81,7 +81,7 @@ frappe.ui.form.on("BOM", { } ) - if (!frm.doc.__islocal && frm.doc.docstatus<2) { + if (!frm.is_new() && frm.doc.docstatus<2) { frm.add_custom_button(__("Update Cost"), function() { frm.events.update_cost(frm, true); }); @@ -93,10 +93,12 @@ frappe.ui.form.on("BOM", { }); } - frm.add_custom_button(__("New Version"), function() { - let new_bom = frappe.model.copy_doc(frm.doc); - frappe.set_route("Form", "BOM", new_bom.name); - }); + if (!frm.is_new() && !frm.doc.docstatus == 0) { + frm.add_custom_button(__("New Version"), function() { + let new_bom = frappe.model.copy_doc(frm.doc); + frappe.set_route("Form", "BOM", new_bom.name); + }); + } if(frm.doc.docstatus==1) { frm.add_custom_button(__("Work Order"), function() { From 3fa0a46f39f7024c5d0b235a7725eaa9ad0f3869 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 9 Jun 2022 16:22:00 +0530 Subject: [PATCH 179/192] chore: Less hacky tests, versioning (replace bom) and clearing log data (update cost) - Remove `auto_commit_on_many_writes` in `update_cost_in_level()` as commits happen every N BOMs - Auto commit every 50 BOMs - test: Remove hacky `frappe.flags.in_test` returns - test: Enqueue `now` if in tests (for update cost and replace bom) - Replace BOM: Copy bom object to `_doc_before_save` so that version.py finds a difference between the two - Replace BOM: Add reference to version - Update Cost: Unset `processed_boms` if Log is completed (useless after completion) - test: `update_cost_in_all_boms_in_test` works close to actual prod implementation (only call Cron job manually) - Test: use `enqueue_replace_bom` so that test works closest to production behaviour Co-authored-by: Ankush Menat --- .../doctype/bom_update_log/bom_update_log.py | 18 ++---- .../bom_update_log/bom_updation_utils.py | 19 ++++--- .../bom_update_log/test_bom_update_log.py | 56 +------------------ .../bom_update_tool/test_bom_update_tool.py | 12 ++-- 4 files changed, 23 insertions(+), 82 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index 71430bd57e..9c9c24044a 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -67,9 +67,6 @@ class BOMUpdateLog(Document): ) def on_submit(self): - if frappe.flags.in_test: - return - if self.update_type == "Replace BOM": boms = {"current_bom": self.current_bom, "new_bom": self.new_bom} frappe.enqueue( @@ -77,6 +74,7 @@ class BOMUpdateLog(Document): doc=self, boms=boms, timeout=40000, + now=frappe.flags.in_test, ) else: process_boms_cost_level_wise(self) @@ -94,7 +92,7 @@ def run_replace_bom_job( frappe.db.auto_commit_on_many_writes = 1 boms = frappe._dict(boms or {}) - replace_bom(boms) + replace_bom(boms, doc.name) doc.db_set("status", "Completed") except Exception: @@ -135,10 +133,6 @@ def process_boms_cost_level_wise( values = {"current_level": current_level} set_values_in_log(update_doc.name, values, commit=True) - - if frappe.flags.in_test: - return current_boms, current_level - queue_bom_cost_jobs(current_boms, update_doc, current_level) @@ -161,16 +155,13 @@ def queue_bom_cost_jobs( ) batch_row.db_insert() - if frappe.flags.in_test: - # skip background jobs in test - return boms_to_process, batch_row.name - frappe.enqueue( method="erpnext.manufacturing.doctype.bom_update_log.bom_updation_utils.update_cost_in_level", doc=update_doc, bom_list=boms_to_process, batch_name=batch_row.name, queue="long", + now=frappe.flags.in_test, ) @@ -208,10 +199,11 @@ def resume_bom_cost_update_jobs(): current_boms, processed_boms = get_processed_current_boms(log, bom_batches) parent_boms = get_next_higher_level_boms(child_boms=current_boms, processed_boms=processed_boms) + # Unset processed BOMs if log is complete, it is used for next level BOMs set_values_in_log( log.name, values={ - "processed_boms": json.dumps(processed_boms), + "processed_boms": json.dumps([] if not parent_boms else processed_boms), "status": "Completed" if not parent_boms else "In Progress", }, commit=True, diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py index dde1e4ed75..af115e3e42 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py @@ -1,6 +1,7 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +import copy import json from collections import defaultdict from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union @@ -12,7 +13,7 @@ import frappe from frappe import _ -def replace_bom(boms: Dict) -> None: +def replace_bom(boms: Dict, log_name: str) -> None: "Replace current BOM with new BOM in parent BOMs." current_bom = boms.get("current_bom") @@ -29,13 +30,17 @@ def replace_bom(boms: Dict) -> None: # this is only used for versioning and we do not want # to make separate db calls by using load_doc_before_save # which proves to be expensive while doing bulk replace - bom_obj._doc_before_save = bom_obj + bom_obj._doc_before_save = copy.deepcopy(bom_obj) bom_obj.update_exploded_items() bom_obj.calculate_cost() bom_obj.update_parent_cost() bom_obj.db_update() - if bom_obj.meta.get("track_changes") and not bom_obj.flags.ignore_version: - bom_obj.save_version() + bom_obj.flags.updater_reference = { + "doctype": "BOM Update Log", + "docname": log_name, + "label": _("via BOM Update Tool"), + } + bom_obj.save_version() def update_cost_in_level( @@ -48,8 +53,6 @@ def update_cost_in_level( if status == "Failed": return - frappe.db.auto_commit_on_many_writes = 1 - update_cost_in_boms(bom_list=bom_list) # main updation logic bom_batch = frappe.qb.DocType("BOM Update Batch") @@ -62,8 +65,6 @@ def update_cost_in_level( except Exception: handle_exception(doc) finally: - frappe.db.auto_commit_on_many_writes = 0 - if not frappe.flags.in_test: frappe.db.commit() # nosemgrep @@ -121,7 +122,7 @@ def update_cost_in_boms(bom_list: List[str]) -> None: bom_doc.calculate_cost(save_updates=True, update_hour_rate=True) bom_doc.db_update() - if (index % 100 == 0) and not frappe.flags.in_test: + if (index % 50 == 0) and not frappe.flags.in_test: frappe.db.commit() # nosemgrep diff --git a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py index d770f6c56a..b38fc8976b 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py @@ -1,22 +1,12 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import json - import frappe from frappe.tests.utils import FrappeTestCase from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import ( BOMMissingError, - get_processed_current_boms, - process_boms_cost_level_wise, - queue_bom_cost_jobs, - run_replace_bom_job, -) -from erpnext.manufacturing.doctype.bom_update_log.bom_updation_utils import ( - get_next_higher_level_boms, - set_values_in_log, - update_cost_in_level, + resume_bom_cost_update_jobs, ) from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import ( enqueue_replace_bom, @@ -60,62 +50,22 @@ class TestBOMUpdateLog(FrappeTestCase): with self.assertRaises(frappe.ValidationError): enqueue_replace_bom(boms=frappe._dict(current_bom=self.boms.new_bom, new_bom="Dummy BOM")) - def test_bom_update_log_queueing(self): - "Test if BOM Update Log is created and queued." - - log = enqueue_replace_bom(boms=self.boms) - - self.assertEqual(log.docstatus, 1) - self.assertEqual(log.status, "Queued") - def test_bom_update_log_completion(self): "Test if BOM Update Log handles job completion correctly." log = enqueue_replace_bom(boms=self.boms) - - # Is run via background job IRL - run_replace_bom_job(doc=log, boms=self.boms) log.reload() - self.assertEqual(log.status, "Completed") def update_cost_in_all_boms_in_test(): """ - Utility to run 'Update Cost' job in tests immediately without Cron job. - Run job for all levels (manually) until fully complete. + Utility to run 'Update Cost' job in tests without Cron job until fully complete. """ - parent_boms = [] log = enqueue_update_cost() # create BOM Update Log while log.status != "Completed": - level_boms, current_level = process_boms_cost_level_wise(log, parent_boms) - log.reload() - - boms, batch = queue_bom_cost_jobs( - level_boms, log, current_level - ) # adds rows in log for tracking - log.reload() - - update_cost_in_level(log, boms, batch) # business logic - log.reload() - - # current level done, get next level boms - bom_batches = frappe.db.get_all( - "BOM Update Batch", - {"parent": log.name, "level": log.current_level}, - ["name", "boms_updated", "status"], - ) - current_boms, processed_boms = get_processed_current_boms(log, bom_batches) - parent_boms = get_next_higher_level_boms(child_boms=current_boms, processed_boms=processed_boms) - - set_values_in_log( - log.name, - values={ - "processed_boms": json.dumps(processed_boms), - "status": "Completed" if not parent_boms else "In Progress", - }, - ) + resume_bom_cost_update_jobs() # run cron job until complete log.reload() return log diff --git a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py index d1882e56e9..5dd557f8ab 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py @@ -4,10 +4,10 @@ import frappe from frappe.tests.utils import FrappeTestCase -from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import replace_bom from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import ( update_cost_in_all_boms_in_test, ) +from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import enqueue_replace_bom from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.stock.doctype.item.test_item import create_item @@ -17,6 +17,9 @@ test_records = frappe.get_test_records("BOM") class TestBOMUpdateTool(FrappeTestCase): "Test major functions run via BOM Update Tool." + def tearDown(self): + frappe.db.rollback() + def test_replace_bom(self): current_bom = "BOM-_Test Item Home Desktop Manufactured-001" @@ -25,16 +28,11 @@ class TestBOMUpdateTool(FrappeTestCase): bom_doc.insert() boms = frappe._dict(current_bom=current_bom, new_bom=bom_doc.name) - replace_bom(boms) + enqueue_replace_bom(boms=boms) self.assertFalse(frappe.db.exists("BOM Item", {"bom_no": current_bom, "docstatus": 1})) self.assertTrue(frappe.db.exists("BOM Item", {"bom_no": bom_doc.name, "docstatus": 1})) - # reverse, as it affects other testcases - boms.current_bom = bom_doc.name - boms.new_bom = current_bom - replace_bom(boms) - def test_bom_cost(self): for item in ["BOM Cost Test Item 1", "BOM Cost Test Item 2", "BOM Cost Test Item 3"]: item_doc = create_item(item, valuation_rate=100) From e6f65e1697dfac82688623184ba4b1fb6782da54 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 9 Jun 2022 17:47:15 +0530 Subject: [PATCH 180/192] chore: Asset Arabic translation Fix (backport #31221) (#31301) chore: Asset Arabic translation Fix (#31221) Update ar.csv Fix Translation arabic translation that caused an error when submitting an asset if user language was arabic (cherry picked from commit 9347cbbc9f7826116faf22db7bf9f3bf32e6e3c2) Co-authored-by: meaziz --- erpnext/translations/ar.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/translations/ar.csv b/erpnext/translations/ar.csv index 91a9da9f16..e62f61a4f5 100644 --- a/erpnext/translations/ar.csv +++ b/erpnext/translations/ar.csv @@ -4297,7 +4297,7 @@ Fetch Serial Numbers based on FIFO,إحضار الأرقام المسلسلة ب "To allow different rates, disable the {0} checkbox in {1}.",للسماح بمعدلات مختلفة ، قم بتعطيل مربع الاختيار {0} في {1}., Current Odometer Value should be greater than Last Odometer Value {0},يجب أن تكون قيمة عداد المسافات الحالية أكبر من قيمة آخر عداد المسافات {0}, No additional expenses has been added,لم يتم إضافة مصاريف إضافية, -Asset{} {assets_link} created for {},الأصل {} {asset_link} الذي تم إنشاؤه لـ {}, +Asset{} {assets_link} created for {},الأصل {} {assets_link} الذي تم إنشاؤه لـ {}, Row {}: Asset Naming Series is mandatory for the auto creation for item {},الصف {}: سلسلة تسمية الأصول إلزامية للإنشاء التلقائي للعنصر {}, Assets not created for {0}. You will have to create asset manually.,لم يتم إنشاء الأصول لـ {0}. سيكون عليك إنشاء الأصل يدويًا., {0} {1} has accounting entries in currency {2} for company {3}. Please select a receivable or payable account with currency {2}.,{0} يحتوي {1} على إدخالات محاسبية بالعملة {2} للشركة {3}. الرجاء تحديد حساب مستحق أو دائن بالعملة {2}., From 17887cde7122ff2332f92394cfa2c8d1e196339a Mon Sep 17 00:00:00 2001 From: RJPvT <48353029+RJPvT@users.noreply.github.com> Date: Wed, 8 Jun 2022 10:55:15 +0200 Subject: [PATCH 181/192] fix: locale Currency and Float setting in update_employee In fieldtypes locale settings (example NL) . and , changes whereby the field is inproperly filled --- erpnext/hr/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index c730b19924..3f4e31b1b2 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -55,6 +55,8 @@ def update_employee_work_history(employee, details, date=None, cancel=False): new_data = getdate(new_data) elif fieldtype == "Datetime" and new_data: new_data = get_datetime(new_data) + elif fieldtype in ["Currency", "Float"] and new_data: + new_data = flt(new_data) setattr(employee, item.fieldname, new_data) if item.fieldname in ["department", "designation", "branch"]: internal_work_history[item.fieldname] = item.new From 2675751d6c2ce188b1df8be5f930869a97ebd520 Mon Sep 17 00:00:00 2001 From: Vladislav Date: Thu, 9 Jun 2022 16:16:08 +0300 Subject: [PATCH 182/192] fix: update ru translate (#31200) * Update ru.csv * Update ru.csv * Update ru.csv * Update ru.csv * Update ru.csv * Update ru.csv * Update ru.csv * Update ru.csv fix logic * Update ru.csv * Update ru.csv * Update ru.csv --- erpnext/translations/ru.csv | 141 +++++++++++++++++++----------------- 1 file changed, 74 insertions(+), 67 deletions(-) diff --git a/erpnext/translations/ru.csv b/erpnext/translations/ru.csv index 6b766e7dc0..743b29493c 100644 --- a/erpnext/translations/ru.csv +++ b/erpnext/translations/ru.csv @@ -288,7 +288,7 @@ Asset {0} must be submitted,Актив {0} должен быть проведе Assets,Активы, Assign,Назначить, Assign Salary Structure,Назначить структуру заработной платы, -Assign To,Назначить в, +Assign To,Назначить для, Assign to Employees,Назначить сотрудникам, Assigning Structures...,Назначение структур..., Associate,Помощник, @@ -421,7 +421,7 @@ Buildings,Здания, Bundle items at time of sale.,Собирать продукты в момент продажи., Business Development Manager,Менеджер по развитию бизнеса, Buy,Купить, -Buying,Покупки, +Buying,Закупки, Buying Amount,Сумма покупки, Buying Price List,Ценовой список покупок, Buying Rate,Частота покупки, @@ -490,7 +490,7 @@ Capital Equipments,Капитальные оборудование, Capital Stock,Капитал, Capital Work in Progress,Капитальная работа в процессе, Cart,Корзина, -Cart is Empty,Корзина Пусто, +Cart is Empty,Корзина пуста, Case No(s) already in use. Try from Case No {0},Случай Нет (ы) уже используется. Попробуйте из дела № {0}, Cash,Наличные, Cash Flow Statement,О движении денежных средств, @@ -578,7 +578,7 @@ Compensatory Off,Компенсационные Выкл, Compensatory leave request days not in valid holidays,Дни запроса на получение компенсационных отчислений не действительны, Complaint,Жалоба, Completion Date,Дата завершения, -Computer,компьютер, +Computer,Компьютер, Condition,Условия, Configure,Конфигурировать, Configure {0},Настроить {0}, @@ -643,7 +643,6 @@ Course Code: ,Код курса: , Course Enrollment {0} does not exists,Зачисление на курс {0} не существует, Course Schedule,Расписание курса, Course: ,Курс: , -Cr,Cr, Create,Создать, Create BOM,Создать спецификацию, Create Delivery Trip,Создать маршрут доставки, @@ -795,7 +794,6 @@ Defense,Оборона, Define Project type.,Установите тип проекта., Define budget for a financial year.,Определить бюджет на финансовый год., Define various loan types,Определение различных видов кредита, -Del,Del, Delay in payment (Days),Задержка в оплате (дни), Delete all the Transactions for this Company,Удалить все транзакции этой компании, Deletion is not permitted for country {0},Для страны не разрешено удаление {0}, @@ -1287,12 +1285,12 @@ Installing presets,Установка пресетов, Institute Abbreviation,институт Аббревиатура, Institute Name,Название института, Instructor,Инструктор, -Insufficient Stock,Недостаточный Stock, -Insurance Start date should be less than Insurance End date,"Дата страхование начала должна быть меньше, чем дата страхование End", +Insufficient Stock,Недостаточный запас, +Insurance Start date should be less than Insurance End date,"Дата начала страхования должна быть раньше, чем дата окончания", Integrated Tax,Интегрированный налог, Inter-State Supplies,Межгосударственные поставки, -Interest Amount,Проценты Сумма, -Interests,интересы, +Interest Amount,Сумма процентов, +Interests,Интересы, Intern,Стажер, Internet Publishing,Интернет издания, Intra-State Supplies,Внутригосударственные поставки, @@ -1397,7 +1395,7 @@ Job Card,Карточка работы, Job Description,Описание работы, Job Offer,Предложение работы, Job card {0} created,Карта работы {0} создана, -Jobs,Работы, +Jobs,Вакансии, Join,Присоединиться, Journal Entries {0} are un-linked,Записи в журнале {0} не-связаны, Journal Entry,Запись в журнале, @@ -1925,7 +1923,7 @@ Pending Amount,В ожидании Сумма, Pending Leaves,Ожидающие листья, Pending Qty,В ожидании кол-во, Pending Quantity,Количество в ожидании, -Pending Review,В ожидании отзыв, +Pending Review,В ожидании отзыва, Pending activities for today,В ожидании деятельность на сегодняшний день, Pension Funds,Пенсионные фонды, Percentage Allocation should be equal to 100%,Процент Распределение должно быть равно 100%, @@ -1949,7 +1947,7 @@ Planned Qty,Планируемое кол-во, Planning,Планирование, Plants and Machineries,Растения и Механизмов, Please Set Supplier Group in Buying Settings.,Установите группу поставщиков в разделе «Настройки покупок»., -Please add a Temporary Opening account in Chart of Accounts,"Пожалуйста, добавьте временный вступительный счет в План счетов", +Please add a Temporary Opening account in Chart of Accounts,"Пожалуйста, добавьте временный вступительный счет в план счетов", Please add the account to root level Company - ,"Пожалуйста, добавьте счет на корневой уровень компании -", Please add the remaining benefits {0} to any of the existing component,Добавьте оставшиеся преимущества {0} к любому из существующих компонентов, Please check Multi Currency option to allow accounts with other currency,"Пожалуйста, проверьте мультивалютный вариант, позволяющий счета другой валюте", @@ -2146,7 +2144,7 @@ Preview Salary Slip,Просмотр Зарплата скольжению, Previous Financial Year is not closed,Предыдущий финансовый год не закрыт, Price,Цена, Price List,Прайс-лист, -Price List Currency not selected,Прайс-лист Обмен не выбран, +Price List Currency not selected,Валюта прайс-листа не выбрана, Price List Rate,Прайс-лист Оценить, Price List master.,Мастер Прайс-лист., Price List must be applicable for Buying or Selling,Прайс-лист должен быть применим для покупки или продажи, @@ -2347,7 +2345,7 @@ Remaining,Осталось, Remaining Balance,Остаток средств, Remarks,Примечания, Reminder to update GSTIN Sent,Напоминание об обновлении отправленного GSTIN, -Remove item if charges is not applicable to that item,"Удалить продукт, если сборы не применимы к этому продукту", +Remove item if charges is not applicable to that item,"Удалить объект, если к нему не применяются сборы", Removed items with no change in quantity or value.,Удалены пункты без изменения в количестве или стоимости., Reopen,Возобновить, Reorder Level,Уровень переупорядочения, @@ -2509,7 +2507,7 @@ Salary Slip of employee {0} already created for this period,Зарплата С Salary Slip of employee {0} already created for time sheet {1},Зарплата Скольжение работника {0} уже создан для табеля {1}, Salary Slip submitted for period from {0} to {1},"Зарплатная ведомость отправлена за период с {0} по {1}", Salary Structure Assignment for Employee already exists,Присвоение структуры зарплаты сотруднику уже существует, -Salary Structure Missing,Структура заработной платы Отсутствующий, +Salary Structure Missing,Структура заработной платы отсутствует, Salary Structure must be submitted before submission of Tax Ememption Declaration,Структура заработной платы должна быть представлена до подачи декларации об освобождении от налогов, Salary Structure not found for employee {0} and date {1},Структура зарплаты не найдена для сотрудника {0} и даты {1}, Salary Structure should have flexible benefit component(s) to dispense benefit amount,Структура заработной платы должна иметь гибкий компонент (ы) выгоды для распределения суммы пособия, @@ -2701,10 +2699,10 @@ Setup default values for POS Invoices,Настройка значений по Setup mode of POS (Online / Offline),Режим настройки POS (Online / Offline), Setup your Institute in ERPNext,Установите свой институт в ERPNext, Share Balance,Баланс акций, -Share Ledger,Поделиться записями, +Share Ledger,Записи по акциям, Share Management,Управление долями, Share Transfer,Передача акций, -Share Type,Share Тип, +Share Type,Тип акций, Shareholder,Акционер, Ship To State,Корабль в штат, Shipments,Поставки, @@ -2796,8 +2794,8 @@ Stock Entry {0} is not submitted,Складской акт {0} не провед Stock Expenses,Расходы по Запасам, Stock In Hand,Запасы на руках, Stock Items,Позиции на складе, -Stock Ledger,Книга учета Запасов, -Stock Ledger Entries and GL Entries are reposted for the selected Purchase Receipts,Записи складской книги и записи GL запасов отправляются для выбранных покупок, +Stock Ledger,Книга учета запасов, +Stock Ledger Entries and GL Entries are reposted for the selected Purchase Receipts,Записи книги учета запасов и записи GL повторно публикуются для выбранных квитанций о покупках, Stock Levels,Уровень запасов, Stock Liabilities,Обязательства по запасам, Stock Options,Опционы, @@ -2829,9 +2827,9 @@ Student Email ID,Идентификация студента по электро Student Group,Учебная группа, Student Group Strength,Сила студенческой группы, Student Group is already updated.,Студенческая группа уже обновлена., -Student Group: ,Студенческая группа:, +Student Group: ,Студенческая группа: , Student ID,Студенческий билет, -Student ID: ,Студенческий билет:, +Student ID: ,Студенческий билет: , Student LMS Activity,Студенческая LMS Активность, Student Mobile No.,Мобильный номер студента, Student Name,Имя ученика, @@ -2864,9 +2862,9 @@ Successfully created payment entries,Успешно созданные плат Successfully deleted all transactions related to this company!,"Успешно удален все сделки, связанные с этой компанией!", Sum of Scores of Assessment Criteria needs to be {0}.,Сумма десятков критериев оценки должно быть {0}., Sum of points for all goals should be 100. It is {0},Сумма баллов за все цели должны быть 100. Это {0}, -Summary,Резюме, -Summary for this month and pending activities,Резюме для этого месяца и в ожидании деятельности, -Summary for this week and pending activities,Резюме на этой неделе и в ожидании деятельности, +Summary,Сводка, +Summary for this month and pending activities,Сводка за этот месяц и предстоящие мероприятия, +Summary for this week and pending activities,Сводка за эту неделю и предстоящие мероприятия, Sunday,Воскресенье, Suplier,Поставщик, Supplier,Поставщик, @@ -2880,7 +2878,7 @@ Supplier Name,наименование поставщика, Supplier Part No,Деталь поставщика №, Supplier Quotation,Предложение поставщика, Supplier Scorecard,Оценочная карта поставщика, -Supplier Warehouse mandatory for sub-contracted Purchase Receipt,Поставщик Склад обязательным для субподрядчиком ТОВАРНЫЙ ЧЕК, +Supplier Warehouse mandatory for sub-contracted Purchase Receipt,Наличие склада поставщика обязательно для субподрядной квитанции о покупке, Supplier database.,База данных поставщиков., Supplier {0} not found in {1},Поставщик {0} не найден в {1}, Supplier(s),Поставщик(и), @@ -3199,7 +3197,7 @@ Used Leaves,Используемые листы, User,Пользователь, User ID,ID пользователя, User ID not set for Employee {0},ID пользователя не установлен для сотрудника {0}, -User Remark,Примечание Пользователь, +User Remark,Примечание пользователя, User has not applied rule on the invoice {0},Пользователь не применил правило к счету {0}, User {0} already exists,Пользователь {0} уже существует, User {0} created,Пользователь {0} создан, @@ -3243,7 +3241,7 @@ View Fees Records,Посмотреть рекорды, View Form,Посмотреть форму, View Lab Tests,Просмотр лабораторных тестов, View Leads,Посмотреть лиды, -View Ledger,Посмотреть Леджер, +View Ledger,Посмотреть записи, View Now,Просмотр сейчас, View a list of all the help videos,Просмотреть список всех справочных видео, View in Cart,Смотрите в корзину, @@ -3314,7 +3312,7 @@ Work Orders Created: {0},Созданы рабочие задания: {0}, Work Summary for {0},Резюме работы для {0}, Work-in-Progress Warehouse is required before Submit,Работа-в-Прогресс Склад требуется перед Отправить, Workflow,Рабочий процесс, -Working,Работающий, +Working,В работе, Working Hours,Часы работы, Workstation,Рабочее место, Workstation is closed on the following dates as per Holiday List: {0},Рабочая место закрыто в следующие даты согласно списка праздников: {0}, @@ -3869,13 +3867,17 @@ Non stock items,Нет на складе, Not Allowed,Не разрешено, Not allowed to create accounting dimension for {0},Не разрешено создавать учетное измерение для {0}, Not permitted. Please disable the Lab Test Template,"Не разрешено Пожалуйста, отключите шаблон лабораторного теста", -Note,Заметки, +Note,Заметка, Notes: ,Заметки: , -On Converting Opportunity,О возможности конвертации, -On Purchase Order Submission,При подаче заказа на поставку, -On Sales Order Submission,На подаче заказа клиента, -On Task Completion,По завершении задачи, +On Converting Opportunity,Конвертацию возможности, +On Purchase Order Submission,Офомление заказа на закупку, +On Sales Order Submission,Оформление заказа на продажу, +On Task Completion,Завершении задачи, On {0} Creation,На {0} создании, +On Item Creation,Создание продукта, +On Lead Creation,Создание лида, +On Supplier Creation,Создание поставщика, +On Customer Creation,Создание клиента, Only .csv and .xlsx files are supported currently,В настоящее время поддерживаются только файлы .csv и .xlsx, Only expired allocation can be cancelled,Только истекшее распределение может быть отменено, Only users with the {0} role can create backdated leave applications,Только пользователи с ролью {0} могут создавать оставленные приложения с задним сроком действия, @@ -4217,7 +4219,7 @@ Mode Of Payment,Способ оплаты, No students Found,Студенты не найдены, Not in Stock,Нет в наличии, Please select a Customer,Выберите клиента, -Printed On,Отпечатано на, +Printed On,Напечатано на, Received From,Получено от, Sales Person,Продавец, To date cannot be before From date,На сегодняшний день не может быть раньше От даты, @@ -4945,14 +4947,14 @@ Max Qty,Макс. кол-во, Min Amt,Мин Amt, Max Amt,Макс Амт, Period Settings,Настройки периода, -Margin,Разница, +Margin,Маржа, Margin Type,Тип маржа, Margin Rate or Amount,Маржинальная ставка или сумма, Price Discount Scheme,Схема скидок, Rate or Discount,Стоимость или скидка, Discount Percentage,Скидка в процентах, Discount Amount,Сумма скидки, -For Price List,Для Прейскурантом, +For Price List,Для прайс-листа, Product Discount Scheme,Схема скидок на товары, Same Item,Тот же пункт, Free Item,Бесплатный товар, @@ -5385,18 +5387,18 @@ Insurance Start Date,Дата начала страхования, Insurance End Date,Дата окончания страхования, Comprehensive Insurance,Комплексное страхование, Maintenance Required,Требуется техническое обслуживание, -Check if Asset requires Preventive Maintenance or Calibration,"Проверьте, требуется ли Asset профилактическое обслуживание или калибровка", +Check if Asset requires Preventive Maintenance or Calibration,"Проверьте, требует ли актив профилактического обслуживания или калибровки", Booked Fixed Asset,Забронированные основные средства, Purchase Receipt Amount,Сумма покупки, Default Finance Book,Финансовая книга по умолчанию, Quality Manager,Менеджер по качеству, -Asset Category Name,Asset Категория Название, +Asset Category Name,Название категории активов, Depreciation Options,Варианты амортизации, Enable Capital Work in Progress Accounting,Включить капитальную работу в процессе учета, Finance Book Detail,Финансовая книга, Asset Category Account,Счет категории активов, Fixed Asset Account,Счет учета основных средств, -Accumulated Depreciation Account,Начисленной амортизации Счет, +Accumulated Depreciation Account,Счет накопленной амортизации, Depreciation Expense Account,Износ счет расходов, Capital Work In Progress Account,Счет капитальной работы, Asset Finance Book,Финансовая книга по активам, @@ -5441,7 +5443,7 @@ Failure Date,Дата отказа, Assign To Name,Назначить имя, Repair Status,Статус ремонта, Error Description,Описание ошибки, -Downtime,время простоя, +Downtime,Время простоя, Repair Cost,Стоимость ремонта, Manufacturing Manager,Менеджер производства, Current Asset Value,Текущая стоимость актива, @@ -6073,7 +6075,7 @@ Shopify Tax/Shipping Title,Изменить название налога / до ERPNext Account,Учетная запись ERPNext, Shopify Webhook Detail,Узнайте подробности веб-камеры, Webhook ID,Идентификатор Webhook, -Tally Migration,Tally Migration, +Tally Migration,Tally миграция, Master Data,Основные данные, "Data exported from Tally that consists of the Chart of Accounts, Customers, Suppliers, Addresses, Items and UOMs","Данные, экспортированные из Tally, которые состоят из плана счетов, клиентов, поставщиков, адресов, позиций и единиц измерения", Is Master Data Processed,Обработка основных данных, @@ -6082,7 +6084,7 @@ Tally Creditors Account,Счет Tally Creditors, Creditors Account set in Tally,Счет кредиторов установлен в Tally, Tally Debtors Account,Счет Tally должников, Debtors Account set in Tally,Счет дебитора установлен в Tally, -Tally Company,Талли Компания, +Tally Company,Tally Компания, Company Name as per Imported Tally Data,Название компании согласно импортированным данным подсчета, Default UOM,Единица измерения по умолчанию, UOM in case unspecified in imported data,"Единицы измерения, если они не указаны в импортированных данных", @@ -6108,7 +6110,7 @@ Freight and Forwarding Account,Фрахт и пересылка, Creation User,Создание пользователя, "The user that will be used to create Customers, Items and Sales Orders. This user should have the relevant permissions.","Пользователь, который будет использоваться для создания клиентов, товаров и заказов на продажу. Этот пользователь должен иметь соответствующие разрешения.", "This warehouse will be used to create Sales Orders. The fallback warehouse is ""Stores"".",Этот склад будет использоваться для создания заказов на продажу. Резервный склад "Магазины"., -"The fallback series is ""SO-WOO-"".",Аварийная серия "SO-WOO-"., +"The fallback series is ""SO-WOO-"".","Аварийная серия ""SO-WOO-"".", This company will be used to create Sales Orders.,Эта компания будет использоваться для создания заказов на продажу., Delivery After (Days),Доставка после (дней), This is the default offset (days) for the Delivery Date in Sales Orders. The fallback offset is 7 days from the order placement date.,Это смещение по умолчанию (дни) для даты поставки в заказах на продажу. Смещение отступления составляет 7 дней с даты размещения заказа., @@ -6441,7 +6443,7 @@ Job Applicant,Соискатель работы, Applicant Name,Имя заявителя, Appointment Date,Назначенная дата, Appointment Letter Template,Шаблон письма о назначении, -Body,Тело, +Body,Содержимое, Closing Notes,Заметки, Appointment Letter content,Письмо о назначении, Appraisal,Оценка, @@ -6455,7 +6457,7 @@ Appraisal Goal,Цель оценки, Key Responsibility Area,Основная зона ответственности, Weightage (%),Весовая нагрузка (%), Score (0-5),Оценка (0-5), -Score Earned,Оценка Заработано, +Score Earned,Оценка получена, Appraisal Template Title,Название шаблона оценки, Appraisal Template Goal,Цель шаблона оценки, KRA,КРА, @@ -6747,7 +6749,7 @@ Applicant Email Address,Адрес электронной почты заяви Awaiting Response,В ожидании ответа, Job Offer Terms,Условия работы, Select Terms and Conditions,Выберите Сроки и условия, -Printing Details,Печатать Подробности, +Printing Details,Подробности печати, Job Offer Term,Срок действия предложения, Offer Term,Условие предложения, Value / Description,Значение / Описание, @@ -7520,7 +7522,7 @@ Expected Time (in hours),Ожидаемое время (в часах), Is Milestone,Является этапом, Task Description,Описание задания, Dependencies,Зависимости, -Dependent Tasks,Зависимые задачи, +Dependent Tasks,Зависит от задач, Depends on Tasks,Зависит от задач, Actual Start Date (via Time Sheet),Фактическая дата начала (по табелю учета рабочего времени), Actual Time (in hours),Фактическое время (в часах), @@ -7645,7 +7647,7 @@ Campaign Schedules,Расписание кампаний, Buyer of Goods and Services.,Покупатель товаров и услуг., CUST-.YYYY.-,CUST-.YYYY.-, Default Company Bank Account,Стандартный банковский счет компании, -From Lead,Из Лида, +From Lead,Из лида, Account Manager,Менеджер по работе с клиентами, Allow Sales Invoice Creation Without Sales Order,Разрешить создание счета без заказа на продажу, Allow Sales Invoice Creation Without Delivery Note,Разрешить создание счета без накладной, @@ -7818,14 +7820,14 @@ Phone No,Номер телефона, Company Description,Описание компании, Registration Details,Регистрационные данные, Company registration numbers for your reference. Tax numbers etc.,Регистрационные номера компании для вашей справки. Налоговые числа и т.д., -Delete Company Transactions,Удалить Сделки Компания, +Delete Company Transactions,Удалить транзакции компании, Currency Exchange,Курс обмена валюты, Specify Exchange Rate to convert one currency into another,Укажите Курс конвертировать одну валюту в другую, From Currency,Из валюты, To Currency,В валюту, For Buying,Для покупки, For Selling,Для продажи, -Customer Group Name,Группа Имя клиента, +Customer Group Name,Название группы клиентов, Parent Customer Group,Родительская группа клиента, Only leaf nodes are allowed in transaction,Только листовые узлы допускаются в сделке, Mention if non-standard receivable account applicable,Упоминание если нестандартная задолженность счет применимо, @@ -7893,7 +7895,7 @@ This is the number of the last created transaction with this prefix,Это чи Update Series Number,Обновить Идентификаторы по Номеру, Quotation Lost Reason,Причина Отказа от Предложения, A third party distributor / dealer / commission agent / affiliate / reseller who sells the companies products for a commission.,"Сторонний дистрибьютер, дилер, агент, филиал или реселлер, который продаёт продукты компании за комиссионное вознаграждение.", -Sales Partner Name,Имя Партнера по продажам, +Sales Partner Name,Имя партнера по продажам, Partner Type,Тип партнера, Address & Contacts,Адрес и контакты, Address Desc,Адрес по убыванию, @@ -7914,7 +7916,7 @@ Sales Person Targets,Цели продавца, Set targets Item Group-wise for this Sales Person.,Задайте цели Продуктовых Групп для Продавца, Supplier Group Name,Название группы поставщиков, Parent Supplier Group,Родительская группа поставщиков, -Target Detail,Цель Подробности, +Target Detail,Подробности цели, Target Qty,Целевое количество, Target Amount,Целевая сумма, Target Distribution,Распределение цели, @@ -7973,13 +7975,13 @@ Is Return,Является Вернуться, Issue Credit Note,Кредитная кредитная карта, Return Against Delivery Note,Вернуться На накладной, Customer's Purchase Order No,Клиентам Заказ Нет, -Billing Address Name,Адрес для выставления счета Имя, +Billing Address Name,Название адреса для выставления счета, Required only for sample item.,Требуется только для образца пункта., "If you have created a standard template in Sales Taxes and Charges Template, select one and click on the button below.","Если вы создали стандартный шаблон в шаблонах Налоги с налогами и сбором платежей, выберите его и нажмите кнопку ниже.", In Words will be visible once you save the Delivery Note.,По словам будет виден только вы сохраните накладной., In Words (Export) will be visible once you save the Delivery Note.,В Слов (Экспорт) будут видны только вы сохраните накладной., Transporter Info,Информация для транспортировки, -Driver Name,Имя драйвера, +Driver Name,Имя водителя, Track this Delivery Note against any Project,Подписка на Delivery Note против любого проекта, Inter Company Reference,Справочник Интер, Print Without Amount,Распечатать без суммы, @@ -8079,7 +8081,7 @@ Delivered by Supplier (Drop Ship),Доставка поставщиком, Supplier Items,Продукты поставщика, Foreign Trade Details,Сведения о внешней торговле, Country of Origin,Страна происхождения, -Sales Details,Продажи Подробности, +Sales Details,Детали продажи, Default Sales Unit of Measure,Единица измерения продаж по умолчанию, Is Sales Item,Продаваемый продукт, Max Discount (%),Макс. скидка (%), @@ -8117,7 +8119,7 @@ Item Alternative,Альтернативный продукт, Alternative Item Code,Альтернативный код продукта, Two-way,Двусторонний, Alternative Item Name,Альтернативное название продукта, -Attribute Name,Имя атрибута, +Attribute Name,Название атрибута, Numeric Values,Числовые значения, From Range,От хребта, Increment,Приращение, @@ -8236,8 +8238,8 @@ Transporter Details,Детали транспорта, Vehicle Number,Номер транспортного средства, Vehicle Date,Дата транспортного средства, Received and Accepted,Получил и принял, -Accepted Quantity,Принято Количество, -Rejected Quantity,Отклонен Количество, +Accepted Quantity,Количество принятых, +Rejected Quantity,Количество отклоненных, Accepted Qty as per Stock UOM,Принятое количество в соответствии с единицами измерения запаса, Sample Quantity,Количество образцов, Rate and Amount,Ставку и сумму, @@ -8285,7 +8287,7 @@ Out of AMC,Из КУА, Warranty Period (Days),Гарантийный срок (дней), Serial No Details,Серийный номер подробнее, MAT-STE-.YYYY.-,MAT-STE-.YYYY.-, -Stock Entry Type,Тип входа, +Stock Entry Type,Тип складской записи, Stock Entry (Outward GIT),Вход в акции (внешний GIT), Material Consumption for Manufacture,Потребление материала для производства, Repack,Перепаковать, @@ -8447,7 +8449,7 @@ No of Sent SMS,Кол-во отправленных SMS, Sent To,Отправить, Absent Student Report,Отчет о пропуске занятия, Assessment Plan Status,Статус плана оценки, -Asset Depreciation Ledger,Износ Леджер активов, +Asset Depreciation Ledger,Книга амортизации основных средств, Asset Depreciations and Balances,Активов Амортизация и противовесов, Available Stock for Packing Items,Доступные Запасы для Комплектации Продуктов, Bank Clearance Summary,Банк уплата по счетам итого, @@ -8559,7 +8561,7 @@ Sales Order Trends,Динамика по сделкам, Sales Partner Commission Summary,Сводка комиссий партнеров по продажам, Sales Partner Target Variance based on Item Group,Целевое отклонение партнера по продажам на основе группы товаров, Sales Partner Transaction Summary,Сводка по сделкам с партнерами по продажам, -Sales Partners Commission,Комиссионные Партнеров по продажам, +Sales Partners Commission,Комиссия партнеров по продажам, Invoiced Amount (Exclusive Tax),Сумма счета (без учета налога), Average Commission Rate,Средний уровень комиссии, Sales Payment Summary,Сводка по продажам, @@ -8579,7 +8581,7 @@ Student Fee Collection,Сбор студенческой платы, Student Monthly Attendance Sheet,Ежемесячная посещаемость студентов, Subcontracted Item To Be Received,"Субподрядный предмет, подлежащий получению", Subcontracted Raw Materials To Be Transferred,Субподрядное сырье для передачи, -Supplier Ledger Summary,Список поставщиков, +Supplier Ledger Summary,Сводка книги поставщиков, Supplier-Wise Sales Analytics,Аналитика продаж в разрезе поставщиков, Support Hour Distribution,Распределение поддержки, TDS Computation Summary,Сводка расчетов TDS, @@ -9242,7 +9244,7 @@ Tasks Completed,Задачи выполнены, Tasks Overdue,Просроченные задачи, Completion,Завершение, Provident Fund Deductions,Отчисления в резервный фонд, -Purchase Order Analysis,Анализ заказа на закупку, +Purchase Order Analysis,Анализ заказов на закупку, From and To Dates are required.,Укажите даты от и до., To Date cannot be before From Date.,Дата не может быть раньше даты начала., Qty to Bill,Кол-во к счету, @@ -9267,7 +9269,7 @@ Sales Order Analysis,Анализ заказов на продажу, Amount Delivered,Сумма доставки, Delay (in Days),Задержка (в днях), Group by Sales Order,Группировать по заказу на продажу, - Sales Value,Объем продаж, + Sales Value, Объем продаж, Stock Qty vs Serial No Count,Кол-во на складе по сравнению с серийным номером, Serial No Count,Серийный номер, Work Order Summary,Сводка заказа на работу, @@ -9320,8 +9322,8 @@ Error creating membership entry for {0},Ошибка создания запис A customer is already linked to this Member,Клиент уже привязан к этому участнику, End Date must not be lesser than Start Date,Дата окончания не должна быть меньше даты начала., Employee {0} already has Active Shift {1}: {2},Сотрудник {0} уже имеет активную смену {1}: {2}, - from {0},от {0}, - to {0},в {0}, + from {0}, от {0}, + to {0}, в {0}, Please select Employee first.,"Пожалуйста, сначала выберите сотрудника.", Please set {0} for the Employee or for Department: {1},Установите {0} для сотрудника или отдела: {1}, To Date should be greater than From Date,"Дата до должна быть больше, чем Дата", @@ -9838,3 +9840,8 @@ Enable European Access,Включить европейский доступ, Creating Purchase Order ...,Создание заказа на поставку ..., "Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Выберите поставщика из списка поставщиков по умолчанию для позиций ниже. При выборе Заказ на поставку будет сделан в отношении товаров, принадлежащих только выбранному Поставщику.", Row #{}: You must select {} serial numbers for item {}.,Строка № {}: необходимо выбрать {} серийных номеров для позиции {}., +Items & Pricing,Продукты и цены, +Overdue,Просрочено, +Completed,Завершенно, +Total Tasks,Всего задач, +Build,Конструктор, From b9dbb36d0e55eb4f12e067032f5e7e93875304e3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 9 Jun 2022 18:58:04 +0530 Subject: [PATCH 183/192] chore: Linting Issues --- erpnext/accounts/report/trial_balance/trial_balance.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index af447df52a..26572130d2 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -161,7 +161,9 @@ def get_rootwise_opening_balances(filters, report_type): additional_conditions += " and project = %(project)s" if filters.get("include_default_book_entries"): - additional_conditions += "AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)" + additional_conditions += ( + "AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)" + ) else: additional_conditions += "AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)" From c13e5ad741de68a51ae478727c35dea5fb6f2390 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 9 Jun 2022 19:18:52 +0530 Subject: [PATCH 184/192] fix: Reset represents company on disabling internal customer and supplier (#31302) --- erpnext/buying/doctype/supplier/supplier.py | 3 +++ erpnext/selling/doctype/customer/customer.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index 97d0ba0b9c..43152e89a8 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -84,6 +84,9 @@ class Supplier(TransactionBase): self.save() def validate_internal_supplier(self): + if not self.is_internal_supplier: + self.represents_company = "" + internal_supplier = frappe.db.get_value( "Supplier", { diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 8889a5f939..35e0b0de40 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -141,6 +141,9 @@ class Customer(TransactionBase): ) def validate_internal_customer(self): + if not self.is_internal_customer: + self.represents_company = "" + internal_customer = frappe.db.get_value( "Customer", { From ee2949aa3fa221877d7a16a02e1e3164894f8219 Mon Sep 17 00:00:00 2001 From: Sun Howwrongbum Date: Thu, 9 Jun 2022 19:28:59 +0530 Subject: [PATCH 185/192] fix: typo in sql condition --- erpnext/accounts/report/trial_balance/trial_balance.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 26572130d2..6bd08ad837 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -162,10 +162,10 @@ def get_rootwise_opening_balances(filters, report_type): if filters.get("include_default_book_entries"): additional_conditions += ( - "AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)" + " AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)" ) else: - additional_conditions += "AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)" + additional_conditions += " AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)" accounting_dimensions = get_accounting_dimensions(as_list=False) From 6fc32b83c89eed916e108a6f2ad7e55c144d7fd7 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 10 Jun 2022 11:03:51 +0530 Subject: [PATCH 186/192] fix: revert show title field on Employee doctype (#31312) --- erpnext/hr/doctype/employee/employee.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index a3638e1a65..42479143e7 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -827,7 +827,7 @@ "idx": 24, "image_field": "image", "links": [], - "modified": "2022-04-22 16:21:55.811983", + "modified": "2022-06-10 01:29:32.952091", "modified_by": "Administrator", "module": "HR", "name": "Employee", @@ -872,7 +872,6 @@ ], "search_fields": "employee_name", "show_name_in_global_search": 1, - "show_title_field_in_link": 1, "sort_field": "modified", "sort_order": "DESC", "states": [], From e2c52436da835969ced5a35f9d2ffedddb715dd6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 24 May 2022 11:54:30 +0530 Subject: [PATCH 187/192] refactor: migrate gl to payment ledger using sql --- .../v14_0/migrate_gl_to_payment_ledger.py | 129 +++++++++++++++--- 1 file changed, 113 insertions(+), 16 deletions(-) diff --git a/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py b/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py index c2267aa9af..1e0d20d059 100644 --- a/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py +++ b/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py @@ -1,11 +1,13 @@ import frappe from frappe import qb +from frappe.query_builder import Case +from frappe.query_builder.custom import ConstantColumn +from frappe.query_builder.functions import IfNull from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_dimensions, make_dimension_in_accounting_doctypes, ) -from erpnext.accounts.utils import create_payment_ledger_entry def create_accounting_dimension_fields(): @@ -15,24 +17,119 @@ def create_accounting_dimension_fields(): make_dimension_in_accounting_doctypes(dimension, ["Payment Ledger Entry"]) -def execute(): - # create accounting dimension fields in Payment Ledger - create_accounting_dimension_fields() +def generate_name_for_payment_ledger_entries(gl_entries): + for index, entry in enumerate(gl_entries, 1): + entry.name = index + + +def get_columns(): + columns = [ + "name", + "creation", + "modified", + "modified_by", + "owner", + "docstatus", + "posting_date", + "account_type", + "account", + "party_type", + "party", + "voucher_type", + "voucher_no", + "against_voucher_type", + "against_voucher_no", + "amount", + "amount_in_account_currency", + "account_currency", + "company", + "cost_center", + "due_date", + "finance_book", + ] + + dimensions_and_defaults = get_dimensions() + if dimensions_and_defaults: + for dimension in dimensions_and_defaults[0]: + columns.append(dimension.fieldname) + + return columns + + +def build_insert_query(): + ple = qb.DocType("Payment Ledger Entry") + columns = get_columns() + insert_query = qb.into(ple) + + # build 'insert' columns in query + insert_query = insert_query.columns(tuple(columns)) + + return insert_query + + +def insert_chunk_into_payment_ledger(insert_query, gl_entries): + if gl_entries: + columns = get_columns() + + # build tuple of data with same column order + for entry in gl_entries: + data = () + for column in columns: + data += (entry[column],) + insert_query = insert_query.insert(data) + insert_query.run() + + +def execute(): + if frappe.reload_doc("accounts", "doctype", "payment_ledger_entry"): + # create accounting dimension fields in Payment Ledger + create_accounting_dimension_fields() + + gl = qb.DocType("GL Entry") + account = qb.DocType("Account") - gl = qb.DocType("GL Entry") - accounts = frappe.db.get_list( - "Account", "name", filters={"account_type": ["in", ["Receivable", "Payable"]]}, as_list=True - ) - gl_entries = [] - if accounts: - # get all gl entries on receivable/payable accounts gl_entries = ( qb.from_(gl) - .select("*") - .where(gl.account.isin(accounts)) + .inner_join(account) + .on((gl.account == account.name) & (account.account_type.isin(["Receivable", "Payable"]))) + .select( + gl.star, + ConstantColumn(1).as_("docstatus"), + account.account_type.as_("account_type"), + IfNull(gl.against_voucher_type, gl.voucher_type).as_("against_voucher_type"), + IfNull(gl.against_voucher, gl.voucher_no).as_("against_voucher_no"), + # convert debit/credit to amount + Case() + .when(account.account_type == "Receivable", gl.debit - gl.credit) + .else_(gl.credit - gl.debit) + .as_("amount"), + # convert debit/credit in account currency to amount in account currency + Case() + .when( + account.account_type == "Receivable", + gl.debit_in_account_currency - gl.credit_in_account_currency, + ) + .else_(gl.credit_in_account_currency - gl.debit_in_account_currency) + .as_("amount_in_account_currency"), + ) .where(gl.is_cancelled == 0) + .orderby(gl.creation) .run(as_dict=True) ) - if gl_entries: - # create payment ledger entries for the accounts receivable/payable - create_payment_ledger_entry(gl_entries, 0) + + # primary key(name) for payment ledger records + generate_name_for_payment_ledger_entries(gl_entries) + + # split data into chunks + chunk_size = 1000 + try: + for i in range(0, len(gl_entries), chunk_size): + insert_query = build_insert_query() + insert_chunk_into_payment_ledger(insert_query, gl_entries[i : i + chunk_size]) + frappe.db.commit() + except Exception as err: + frappe.db.rollback() + ple = qb.DocType("Payment Ledger Entry") + qb.from_(ple).delete().where(ple.docstatus >= 0).run() + frappe.db.commit() + raise err From 1646fbe478fefaa173c2c1e009d1a5d0dcb13326 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 10 Jun 2022 13:52:17 +0530 Subject: [PATCH 188/192] refactor: remove add_fetch (#31315) - Sales Team already had fetch from set up - Set up fetch from on sales partner in sales transaction Reason for removal: the JS code applies arbitrarily to any field called "sales_person" --- erpnext/accounts/doctype/sales_invoice/sales_invoice.json | 4 +++- erpnext/selling/doctype/sales_order/sales_order.json | 4 +++- erpnext/selling/sales_common.js | 4 +--- erpnext/stock/doctype/delivery_note/delivery_note.json | 4 +++- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 80b95db886..327545aa54 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -1790,6 +1790,8 @@ "width": "50%" }, { + "fetch_from": "sales_partner.commission_rate", + "fetch_if_empty": 1, "fieldname": "commission_rate", "fieldtype": "Float", "hide_days": 1, @@ -2038,7 +2040,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2022-03-08 16:08:53.517903", + "modified": "2022-06-10 03:52:51.409913", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index ff921c721d..74c5c07e47 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -1359,6 +1359,8 @@ "width": "50%" }, { + "fetch_from": "sales_partner.commission_rate", + "fetch_if_empty": 1, "fieldname": "commission_rate", "fieldtype": "Float", "hide_days": 1, @@ -1547,7 +1549,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2022-04-26 14:38:18.350207", + "modified": "2022-06-10 03:52:22.212953", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 6cb53c3bbe..8ff01f5cb4 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -12,8 +12,6 @@ frappe.provide("erpnext.selling"); erpnext.selling.SellingController = class SellingController extends erpnext.TransactionController { setup() { super.setup(); - this.frm.add_fetch("sales_partner", "commission_rate", "commission_rate"); - this.frm.add_fetch("sales_person", "commission_rate", "commission_rate"); } onload() { @@ -514,4 +512,4 @@ frappe.ui.form.on(cur_frm.doctype, { dialog.show(); } -}) \ No newline at end of file +}) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index e3222bc885..f9e934921d 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -1192,6 +1192,8 @@ "width": "50%" }, { + "fetch_from": "sales_partner.commission_rate", + "fetch_if_empty": 1, "fieldname": "commission_rate", "fieldtype": "Float", "label": "Commission Rate (%)", @@ -1334,7 +1336,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2022-04-26 14:48:08.781837", + "modified": "2022-06-10 03:52:04.197415", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", From 3a3d13622d9cdae52e11b728a196602da70d3ec3 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 10 Jun 2022 14:01:41 +0530 Subject: [PATCH 189/192] refactor!: drop github connector from ERPNext (#31316) --- .../connectors/github_connection.py | 44 ------------------- .../data_migration_mapping/__init__.py | 0 .../issue_to_task/__init__.py | 12 ----- .../issue_to_task/issue_to_task.json | 36 --------------- .../milestone_to_project/__init__.py | 6 --- .../milestone_to_project.json | 36 --------------- .../github_sync/github_sync.json | 22 ---------- requirements.txt | 1 - 8 files changed, 157 deletions(-) delete mode 100644 erpnext/erpnext_integrations/connectors/github_connection.py delete mode 100644 erpnext/erpnext_integrations/data_migration_mapping/__init__.py delete mode 100644 erpnext/erpnext_integrations/data_migration_mapping/issue_to_task/__init__.py delete mode 100644 erpnext/erpnext_integrations/data_migration_mapping/issue_to_task/issue_to_task.json delete mode 100644 erpnext/erpnext_integrations/data_migration_mapping/milestone_to_project/__init__.py delete mode 100644 erpnext/erpnext_integrations/data_migration_mapping/milestone_to_project/milestone_to_project.json delete mode 100644 erpnext/erpnext_integrations/data_migration_plan/github_sync/github_sync.json diff --git a/erpnext/erpnext_integrations/connectors/github_connection.py b/erpnext/erpnext_integrations/connectors/github_connection.py deleted file mode 100644 index f28065e724..0000000000 --- a/erpnext/erpnext_integrations/connectors/github_connection.py +++ /dev/null @@ -1,44 +0,0 @@ -import frappe -from frappe.data_migration.doctype.data_migration_connector.connectors.base import BaseConnection -from github import Github - -class GithubConnection(BaseConnection): - def __init__(self, connector): - self.connector = connector - - try: - password = self.get_password() - except frappe.AuthenticationError: - password = None - - if self.connector.username and password: - self.connection = Github(self.connector.username, self.get_password()) - else: - self.connection = Github() - - self.name_field = 'id' - - def insert(self, doctype, doc): - pass - - def update(self, doctype, doc, migration_id): - pass - - def delete(self, doctype, migration_id): - pass - - def get(self, remote_objectname, fields=None, filters=None, start=0, page_length=10): - repo = filters.get('repo') - - if remote_objectname == 'Milestone': - return self.get_milestones(repo, start, page_length) - if remote_objectname == 'Issue': - return self.get_issues(repo, start, page_length) - - def get_milestones(self, repo, start=0, page_length=10): - _repo = self.connection.get_repo(repo) - return list(_repo.get_milestones()[start:start+page_length]) - - def get_issues(self, repo, start=0, page_length=10): - _repo = self.connection.get_repo(repo) - return list(_repo.get_issues()[start:start+page_length]) diff --git a/erpnext/erpnext_integrations/data_migration_mapping/__init__.py b/erpnext/erpnext_integrations/data_migration_mapping/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/erpnext_integrations/data_migration_mapping/issue_to_task/__init__.py b/erpnext/erpnext_integrations/data_migration_mapping/issue_to_task/__init__.py deleted file mode 100644 index 616ecfbac6..0000000000 --- a/erpnext/erpnext_integrations/data_migration_mapping/issue_to_task/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -import frappe - - -def pre_process(issue): - - project = frappe.db.get_value("Project", filters={"project_name": issue.milestone}) - return { - "title": issue.title, - "body": frappe.utils.md_to_html(issue.body or ""), - "state": issue.state.title(), - "project": project or "", - } diff --git a/erpnext/erpnext_integrations/data_migration_mapping/issue_to_task/issue_to_task.json b/erpnext/erpnext_integrations/data_migration_mapping/issue_to_task/issue_to_task.json deleted file mode 100644 index e945ba2261..0000000000 --- a/erpnext/erpnext_integrations/data_migration_mapping/issue_to_task/issue_to_task.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "condition": "{\"repo\":\"frappe/erpnext\"}", - "creation": "2017-10-16 16:03:32.772191", - "docstatus": 0, - "doctype": "Data Migration Mapping", - "fields": [ - { - "is_child_table": 0, - "local_fieldname": "subject", - "remote_fieldname": "title" - }, - { - "is_child_table": 0, - "local_fieldname": "description", - "remote_fieldname": "body" - }, - { - "is_child_table": 0, - "local_fieldname": "status", - "remote_fieldname": "state" - } - ], - "idx": 0, - "local_doctype": "Task", - "local_primary_key": "name", - "mapping_name": "Issue to Task", - "mapping_type": "Pull", - "migration_id_field": "github_sync_id", - "modified": "2017-10-20 11:48:54.575993", - "modified_by": "Administrator", - "name": "Issue to Task", - "owner": "Administrator", - "page_length": 10, - "remote_objectname": "Issue", - "remote_primary_key": "id" -} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/data_migration_mapping/milestone_to_project/__init__.py b/erpnext/erpnext_integrations/data_migration_mapping/milestone_to_project/__init__.py deleted file mode 100644 index d44fc0454c..0000000000 --- a/erpnext/erpnext_integrations/data_migration_mapping/milestone_to_project/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -def pre_process(milestone): - return { - "title": milestone.title, - "description": milestone.description, - "state": milestone.state.title(), - } diff --git a/erpnext/erpnext_integrations/data_migration_mapping/milestone_to_project/milestone_to_project.json b/erpnext/erpnext_integrations/data_migration_mapping/milestone_to_project/milestone_to_project.json deleted file mode 100644 index 5a3e07e37e..0000000000 --- a/erpnext/erpnext_integrations/data_migration_mapping/milestone_to_project/milestone_to_project.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "condition": "{\"repo\": \"frappe/erpnext\"}", - "creation": "2017-10-13 11:16:49.664925", - "docstatus": 0, - "doctype": "Data Migration Mapping", - "fields": [ - { - "is_child_table": 0, - "local_fieldname": "project_name", - "remote_fieldname": "title" - }, - { - "is_child_table": 0, - "local_fieldname": "notes", - "remote_fieldname": "description" - }, - { - "is_child_table": 0, - "local_fieldname": "status", - "remote_fieldname": "state" - } - ], - "idx": 0, - "local_doctype": "Project", - "local_primary_key": "project_name", - "mapping_name": "Milestone to Project", - "mapping_type": "Pull", - "migration_id_field": "github_sync_id", - "modified": "2017-10-20 11:48:54.552305", - "modified_by": "Administrator", - "name": "Milestone to Project", - "owner": "Administrator", - "page_length": 10, - "remote_objectname": "Milestone", - "remote_primary_key": "id" -} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/data_migration_plan/github_sync/github_sync.json b/erpnext/erpnext_integrations/data_migration_plan/github_sync/github_sync.json deleted file mode 100644 index 20eb387cd8..0000000000 --- a/erpnext/erpnext_integrations/data_migration_plan/github_sync/github_sync.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "creation": "2017-10-13 11:16:53.600026", - "docstatus": 0, - "doctype": "Data Migration Plan", - "idx": 0, - "mappings": [ - { - "enabled": 1, - "mapping": "Milestone to Project" - }, - { - "enabled": 1, - "mapping": "Issue to Task" - } - ], - "modified": "2017-10-20 11:48:54.496123", - "modified_by": "Administrator", - "module": "ERPNext Integrations", - "name": "GitHub Sync", - "owner": "Administrator", - "plan_name": "GitHub Sync" -} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 85ff515772..83e53758be 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,6 @@ gocardless-pro~=1.22.0 googlemaps plaid-python~=7.2.1 pycountry~=20.7.3 -PyGithub~=1.55 python-stdnum~=1.16 python-youtube~=0.8.0 taxjar~=1.9.2 From 74b274f555801356b1ef05577eb4cb2cfb79b8d3 Mon Sep 17 00:00:00 2001 From: hendrik Date: Fri, 10 Jun 2022 16:22:53 +0700 Subject: [PATCH 190/192] fix: update Period Closing Voucher per Company Validate period closing voucher company-wise --- .../doctype/period_closing_voucher/period_closing_voucher.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index 53b1c64c46..5a86376199 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -54,8 +54,8 @@ class PeriodClosingVoucher(AccountsController): pce = frappe.db.sql( """select name from `tabPeriod Closing Voucher` - where posting_date > %s and fiscal_year = %s and docstatus = 1""", - (self.posting_date, self.fiscal_year), + where posting_date > %s and fiscal_year = %s and docstatus = 1 and company = %s""", + (self.posting_date, self.fiscal_year, self.company), ) if pce and pce[0][0]: frappe.throw( From 39ec0aca9550e64bccdbdad96613f25b97026c53 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 10 Jun 2022 18:43:46 +0530 Subject: [PATCH 191/192] fix(UX): use doc.status for Job Card status (#31320) fix(UX): use doc.status for JC status - Use doc.status directly for indicator - single source of truth - Update status to cancelled when doc is cancelled --- .../doctype/job_card/job_card.py | 13 +++++----- .../doctype/job_card/job_card_list.js | 23 +++++++++--------- .../doctype/job_card/test_job_card.py | 24 +++++++++++++++++++ 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 0199a5c31e..ed45106634 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -626,14 +626,15 @@ class JobCard(Document): self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0] - if self.for_quantity <= self.transferred_qty: - self.status = "Material Transferred" + if self.docstatus < 2: + if self.for_quantity <= self.transferred_qty: + self.status = "Material Transferred" - if self.time_logs: - self.status = "Work In Progress" + if self.time_logs: + self.status = "Work In Progress" - if self.docstatus == 1 and (self.for_quantity <= self.total_completed_qty or not self.items): - self.status = "Completed" + if self.docstatus == 1 and (self.for_quantity <= self.total_completed_qty or not self.items): + self.status = "Completed" if update_status: self.db_set("status", self.status) diff --git a/erpnext/manufacturing/doctype/job_card/job_card_list.js b/erpnext/manufacturing/doctype/job_card/job_card_list.js index 7f60bdc6d9..5d883bf9fa 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card_list.js +++ b/erpnext/manufacturing/doctype/job_card/job_card_list.js @@ -1,16 +1,17 @@ frappe.listview_settings['Job Card'] = { has_indicator_for_draft: true, + get_indicator: function(doc) { - if (doc.status === "Work In Progress") { - return [__("Work In Progress"), "orange", "status,=,Work In Progress"]; - } else if (doc.status === "Completed") { - return [__("Completed"), "green", "status,=,Completed"]; - } else if (doc.docstatus == 2) { - return [__("Cancelled"), "red", "status,=,Cancelled"]; - } else if (doc.status === "Material Transferred") { - return [__('Material Transferred'), "blue", "status,=,Material Transferred"]; - } else { - return [__("Open"), "red", "status,=,Open"]; - } + const status_colors = { + "Work In Progress": "orange", + "Completed": "green", + "Cancelled": "red", + "Material Transferred": "blue", + "Open": "red", + }; + const status = doc.status || "Open"; + const color = status_colors[status] || "blue"; + + return [__(status), color, `status,=,${status}`]; } }; diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 7f3c7fefe9..ac7114138c 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -344,6 +344,30 @@ class TestJobCard(FrappeTestCase): cost_after_cancel = self.work_order.total_operating_cost self.assertEqual(cost_after_cancel, original_cost) + def test_job_card_statuses(self): + def assertStatus(status): + jc.set_status() + self.assertEqual(jc.status, status) + + jc = frappe.new_doc("Job Card") + jc.for_quantity = 2 + jc.transferred_qty = 1 + jc.total_completed_qty = 0 + assertStatus("Open") + + jc.transferred_qty = jc.for_quantity + assertStatus("Material Transferred") + + jc.append("time_logs", {}) + assertStatus("Work In Progress") + + jc.docstatus = 1 + jc.total_completed_qty = jc.for_quantity + assertStatus("Completed") + + jc.docstatus = 2 + assertStatus("Cancelled") + def create_bom_with_multiple_operations(): "Create a BOM with multiple operations and Material Transfer against Job Card" From f6695909c47265cccc6dc85744ce6906d2b01136 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 10 Jun 2022 15:51:02 +0200 Subject: [PATCH 192/192] fix: remove DATEV from accounting workspace --- .../workspace/accounting/accounting.json | 35 +++++++------------ 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/erpnext/accounts/workspace/accounting/accounting.json b/erpnext/accounts/workspace/accounting/accounting.json index a456c7fb57..61f6225459 100644 --- a/erpnext/accounts/workspace/accounting/accounting.json +++ b/erpnext/accounts/workspace/accounting/accounting.json @@ -504,18 +504,6 @@ "onboard": 0, "type": "Link" }, - { - "dependencies": "GL Entry", - "hidden": 0, - "is_query_report": 1, - "label": "DATEV Export", - "link_count": 0, - "link_to": "DATEV", - "link_type": "Report", - "onboard": 0, - "only_for": "Germany", - "type": "Link" - }, { "dependencies": "GL Entry", "hidden": 0, @@ -1024,16 +1012,16 @@ "type": "Link" }, { - "dependencies": "Cost Center", - "hidden": 0, - "is_query_report": 0, - "label": "Cost Center Allocation", - "link_count": 0, - "link_to": "Cost Center Allocation", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, + "dependencies": "Cost Center", + "hidden": 0, + "is_query_report": 0, + "label": "Cost Center Allocation", + "link_count": 0, + "link_to": "Cost Center Allocation", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "dependencies": "Cost Center", "hidden": 0, @@ -1235,13 +1223,14 @@ "type": "Link" } ], - "modified": "2022-01-13 17:25:09.835345", + "modified": "2022-06-10 15:49:42.990860", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting", "owner": "Administrator", "parent_page": "", "public": 1, + "quick_lists": [], "restrict_to_domain": "", "roles": [], "sequence_id": 2.0,