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 8e0e62d5f8c2..a944a3738328 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 @@ -78,7 +78,10 @@ def test_cost_center_wise_posting(self): expense_account="Cost of Goods Sold - TPC", rate=400, debit_to="Debtors - TPC", + currency="USD", + customer="_Test Customer USD", ) + create_sales_invoice( company=company, cost_center=cost_center2, @@ -86,6 +89,8 @@ def test_cost_center_wise_posting(self): expense_account="Cost of Goods Sold - TPC", rate=200, debit_to="Debtors - TPC", + currency="USD", + customer="_Test Customer USD", ) pcv = self.make_period_closing_voucher(submit=False) @@ -119,14 +124,17 @@ def test_period_closing_with_finance_book_entries(self): 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/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js index 264d4a68b009..572410fc6651 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js @@ -64,13 +64,15 @@ frappe.ui.form.on('POS Closing Entry', { pos_opening_entry(frm) { if (frm.doc.pos_opening_entry && frm.doc.period_start_date && frm.doc.period_end_date && frm.doc.user) { reset_values(frm); - frm.trigger("set_opening_amounts"); - frm.trigger("get_pos_invoices"); + frappe.run_serially([ + () => frm.trigger("set_opening_amounts"), + () => frm.trigger("get_pos_invoices") + ]); } }, set_opening_amounts(frm) { - frappe.db.get_doc("POS Opening Entry", frm.doc.pos_opening_entry) + return frappe.db.get_doc("POS Opening Entry", frm.doc.pos_opening_entry) .then(({ balance_details }) => { balance_details.forEach(detail => { frm.add_child("payment_reconciliation", { @@ -83,7 +85,7 @@ frappe.ui.form.on('POS Closing Entry', { }, get_pos_invoices(frm) { - frappe.call({ + return frappe.call({ method: 'erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices', args: { start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date), diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 4b81a7d6a239..5701402811ec 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -712,7 +712,7 @@ def test_multiple_pricing_rules_with_min_qty(self): 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() diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index c6a110dcab6e..dfa22641a5e2 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -475,7 +475,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte let row = frappe.get_doc(d.doctype, d.name) set_timesheet_detail_rate(row.doctype, row.name, me.frm.doc.currency, row.timesheet_detail) }); - frm.trigger("calculate_timesheet_totals"); + this.frm.trigger("calculate_timesheet_totals"); } } }); @@ -885,27 +885,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; @@ -916,7 +933,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"); diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index b76ce29b5050..177624ca0322 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -898,3 +898,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/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py index f3ccc868c4cb..c41d0d10ffee 100644 --- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py +++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py @@ -198,10 +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) @@ -214,14 +216,12 @@ 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) loan_docs.extend(entries) @@ -267,15 +267,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"))) diff --git a/erpnext/accounts/report/cash_flow/cash_flow.py b/erpnext/accounts/report/cash_flow/cash_flow.py index 7929d4aa2aef..ee924f86a6a1 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.py +++ b/erpnext/accounts/report/cash_flow/cash_flow.py @@ -263,7 +263,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") ] diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 7d41c84acfca..01586b3de1c7 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -35,6 +35,7 @@ from erpnext.accounts.party import ( get_party_account, get_party_account_currency, + get_party_gle_currency, validate_party_frozen_disabled, ) from erpnext.accounts.utils import get_account_currency, get_fiscal_years, validate_fiscal_year @@ -169,6 +170,7 @@ def validate(self): self.validate_party() self.validate_currency() + self.validate_party_account_currency() if self.doctype in ["Purchase Invoice", "Sales Invoice"]: pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid" @@ -1445,6 +1447,27 @@ def validate_currency(self): # 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 + + 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") + ) + 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: diff --git a/erpnext/erpnext_integrations/connectors/shopify_connection.py b/erpnext/erpnext_integrations/connectors/shopify_connection.py index 4579a274ffa5..f28afbcd83a2 100644 --- a/erpnext/erpnext_integrations/connectors/shopify_connection.py +++ b/erpnext/erpnext_integrations/connectors/shopify_connection.py @@ -4,6 +4,7 @@ from frappe import _ from frappe.utils import cint, cstr, flt, get_datetime, get_request_session, getdate, nowdate +from erpnext import get_company_currency from erpnext.erpnext_integrations.doctype.shopify_log.shopify_log import ( dump_request_data, make_shopify_log, @@ -143,6 +144,10 @@ def create_sales_order(shopify_order, shopify_settings, company=None): "taxes": get_order_taxes(shopify_order, shopify_settings), "apply_discount_on": "Grand Total", "discount_amount": get_discounted_amount(shopify_order), + "currency": frappe.get_value( + "Customer", customer or shopify_settings.default_customer, "default_currency" + ) + or get_company_currency(shopify_settings.company), } ) @@ -178,6 +183,7 @@ def create_sales_invoice(shopify_order, shopify_settings, so, old_order_sync=Fal si.set_posting_time = 1 si.posting_date = posting_date si.due_date = posting_date + si.currency = so.currency si.naming_series = shopify_settings.sales_invoice_series or "SI-Shopify-" si.flags.ignore_mandatory = True set_cost_center(si.items, shopify_settings.cost_center) diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py b/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py index 7cc45d2115f4..47d6d438b065 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py +++ b/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py @@ -58,6 +58,7 @@ def setup_shopify(cls): "warehouse": "_Test Warehouse - _TC", "cash_bank_account": "Cash - _TC", "account": "Cash - _TC", + "company": "_Test Company", "customer_group": "_Test Customer Group", "cost_center": "Main - _TC", "taxes": [{"shopify_tax": "International Shipping", "tax_account": "Legal Expenses - _TC"}], diff --git a/erpnext/healthcare/doctype/lab_test/test_lab_test.py b/erpnext/healthcare/doctype/lab_test/test_lab_test.py index 06c02d1ea079..c08820f36be0 100644 --- a/erpnext/healthcare/doctype/lab_test/test_lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/test_lab_test.py @@ -164,6 +164,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] diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index b6e30060437e..0d98fff04ff7 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -12,6 +12,7 @@ from frappe.model.mapper import get_mapped_doc from frappe.utils import flt, get_link_to_form, get_time, getdate +from erpnext import get_company_currency from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import ( get_income_account, get_receivable_account, @@ -252,6 +253,10 @@ 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.company) + sales_invoice.appointment = appointment_doc.name sales_invoice.due_date = getdate() sales_invoice.company = appointment_doc.company diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py index 048547a93223..05e6b9cfe0d0 100644 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -379,6 +379,7 @@ def create_patient( patient.mobile = mobile patient.email = email patient.customer = customer + patient.default_currency = "INR" patient.invite_user = create_user patient.save(ignore_permissions=True) diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py index 44f0a9785c44..6cb2a24e6af1 100644 --- a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py +++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py @@ -6,6 +6,8 @@ from frappe.model.document import Document from frappe.utils import flt +from erpnext import get_company_currency + class TherapyPlan(Document): def validate(self): @@ -72,6 +74,9 @@ 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( diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.json b/erpnext/hr/doctype/employee_advance/employee_advance.json index 3a561216ccae..8b2eea113379 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.json +++ b/erpnext/hr/doctype/employee_advance/employee_advance.json @@ -2,7 +2,7 @@ "actions": [], "allow_import": 1, "autoname": "naming_series:", - "creation": "2017-10-09 14:26:29.612365", + "creation": "2022-01-17 18:36:51.450395", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", @@ -121,7 +121,7 @@ "fieldtype": "Select", "label": "Status", "no_copy": 1, - "options": "Draft\nPaid\nUnpaid\nClaimed\nCancelled", + "options": "Draft\nPaid\nUnpaid\nClaimed\nReturned\nPartly Claimed and Returned\nCancelled", "read_only": 1 }, { @@ -200,7 +200,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-09-11 18:38:38.617478", + "modified": "2022-05-23 19:33:52.345823", "modified_by": "Administrator", "module": "HR", "name": "Employee Advance", @@ -236,5 +236,6 @@ "search_fields": "employee,employee_name", "sort_field": "modified", "sort_order": "DESC", + "title_field": "employee_name", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index 3d4023d3195a..c1876b117576 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -29,19 +29,43 @@ def validate(self): def on_cancel(self): self.ignore_linked_doctypes = "GL Entry" + self.set_status(update=True) + + def set_status(self, update=False): + precision = self.precision("paid_amount") + total_amount = flt(flt(self.claimed_amount) + flt(self.return_amount), precision) + status = None - def set_status(self): if self.docstatus == 0: - self.status = "Draft" - if self.docstatus == 1: - if self.claimed_amount and flt(self.claimed_amount) == flt(self.paid_amount): - self.status = "Claimed" - elif self.paid_amount and self.advance_amount == flt(self.paid_amount): - self.status = "Paid" + status = "Draft" + elif self.docstatus == 1: + if flt(self.claimed_amount) > 0 and flt(self.claimed_amount, precision) == flt( + self.paid_amount, precision + ): + status = "Claimed" + elif flt(self.return_amount) > 0 and flt(self.return_amount, precision) == flt( + self.paid_amount, precision + ): + status = "Returned" + elif ( + flt(self.claimed_amount) > 0 + and (flt(self.return_amount) > 0) + and total_amount == flt(self.paid_amount, precision) + ): + status = "Partly Claimed and Returned" + elif flt(self.paid_amount) > 0 and flt(self.advance_amount, precision) == flt( + self.paid_amount, precision + ): + status = "Paid" else: - self.status = "Unpaid" + status = "Unpaid" elif self.docstatus == 2: - self.status = "Cancelled" + status = "Cancelled" + + if update: + self.db_set("status", status) + else: + self.status = status def set_total_advance_paid(self): gle = frappe.qb.DocType("GL Entry") @@ -89,8 +113,7 @@ def set_total_advance_paid(self): self.db_set("paid_amount", paid_amount) self.db_set("return_amount", return_amount) - self.set_status() - frappe.db.set_value("Employee Advance", self.name, "status", self.status) + self.set_status(update=True) def update_claimed_amount(self): claimed_amount = ( @@ -112,8 +135,7 @@ def update_claimed_amount(self): frappe.db.set_value("Employee Advance", self.name, "claimed_amount", flt(claimed_amount)) self.reload() - self.set_status() - frappe.db.set_value("Employee Advance", self.name, "status", self.status) + self.set_status(update=True) @frappe.whitelist() @@ -265,6 +287,7 @@ def make_return_entry( "party_type": "Employee", "party": employee, "is_advance": "Yes", + "cost_center": erpnext.get_default_cost_center(company), }, ) @@ -282,6 +305,7 @@ def make_return_entry( "account_currency": bank_cash_account.account_currency, "account_type": bank_cash_account.account_type, "exchange_rate": flt(exchange_rate) if bank_cash_account.account_currency == currency else 1, + "cost_center": erpnext.get_default_cost_center(company), }, ) diff --git a/erpnext/hr/doctype/employee_advance/employee_advance_list.js b/erpnext/hr/doctype/employee_advance/employee_advance_list.js new file mode 100644 index 000000000000..433669a71f3c --- /dev/null +++ b/erpnext/hr/doctype/employee_advance/employee_advance_list.js @@ -0,0 +1,15 @@ +frappe.listview_settings["Employee Advance"] = { + get_indicator: function(doc) { + let status_color = { + "Draft": "red", + "Submitted": "blue", + "Cancelled": "red", + "Paid": "green", + "Unpaid": "orange", + "Claimed": "blue", + "Returned": "gray", + "Partly Claimed and Returned": "yellow" + }; + return [__(doc.status), status_color[doc.status], "status,=,"+doc.status]; + } +}; \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_advance/test_employee_advance.py b/erpnext/hr/doctype/employee_advance/test_employee_advance.py index 9b006ffcffe5..44d68c948335 100644 --- a/erpnext/hr/doctype/employee_advance/test_employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/test_employee_advance.py @@ -12,13 +12,21 @@ EmployeeAdvanceOverPayment, create_return_through_additional_salary, make_bank_entry, + make_return_entry, ) from erpnext.hr.doctype.expense_claim.expense_claim import get_advances +from erpnext.hr.doctype.expense_claim.test_expense_claim import ( + get_payable_account, + make_expense_claim, +) from erpnext.payroll.doctype.salary_component.test_salary_component import create_salary_component from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure class TestEmployeeAdvance(unittest.TestCase): + def setUp(self): + frappe.db.delete("Employee Advance") + def test_paid_amount_and_status(self): employee_name = make_employee("_T@employe.advance") advance = make_employee_advance(employee_name) @@ -53,9 +61,108 @@ def test_paid_amount_on_pe_cancellation(self): self.assertEqual(advance.paid_amount, 0) self.assertEqual(advance.status, "Unpaid") + advance.cancel() + advance.reload() + self.assertEqual(advance.status, "Cancelled") + + def test_claimed_status(self): + # CLAIMED Status check, full amount claimed + payable_account = get_payable_account("_Test Company") + claim = make_expense_claim( + payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True + ) + + advance = make_employee_advance(claim.employee) + pe = make_payment_entry(advance) + pe.submit() + + claim = get_advances_for_claim(claim, advance.name) + claim.save() + claim.submit() + + advance.reload() + self.assertEqual(advance.claimed_amount, 1000) + self.assertEqual(advance.status, "Claimed") + + # advance should not be shown in claims + advances = get_advances(claim.employee) + advances = [entry.name for entry in advances] + self.assertTrue(advance.name not in advances) + + # cancel claim; status should be Paid + claim.cancel() + advance.reload() + self.assertEqual(advance.claimed_amount, 0) + self.assertEqual(advance.status, "Paid") + + def test_partly_claimed_and_returned_status(self): + payable_account = get_payable_account("_Test Company") + claim = make_expense_claim( + payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True + ) + + advance = make_employee_advance(claim.employee) + pe = make_payment_entry(advance) + pe.submit() + + # PARTLY CLAIMED AND RETURNED status check + # 500 Claimed, 500 Returned + claim = make_expense_claim( + payable_account, 500, 500, "_Test Company", "Travel Expenses - _TC", do_not_submit=True + ) + + advance = make_employee_advance(claim.employee) + pe = make_payment_entry(advance) + pe.submit() + + claim = get_advances_for_claim(claim, advance.name, amount=500) + claim.save() + claim.submit() + + advance.reload() + self.assertEqual(advance.claimed_amount, 500) + self.assertEqual(advance.status, "Paid") + + entry = make_return_entry( + employee=advance.employee, + company=advance.company, + employee_advance_name=advance.name, + return_amount=flt(advance.paid_amount - advance.claimed_amount), + advance_account=advance.advance_account, + mode_of_payment=advance.mode_of_payment, + currency=advance.currency, + exchange_rate=advance.exchange_rate, + ) + + entry = frappe.get_doc(entry) + entry.insert() + entry.submit() + + advance.reload() + self.assertEqual(advance.return_amount, 500) + self.assertEqual(advance.status, "Partly Claimed and Returned") + + # advance should not be shown in claims + advances = get_advances(claim.employee) + advances = [entry.name for entry in advances] + self.assertTrue(advance.name not in advances) + + # Cancel return entry; status should change to PAID + entry.cancel() + advance.reload() + self.assertEqual(advance.return_amount, 0) + self.assertEqual(advance.status, "Paid") + + # advance should be shown in claims + advances = get_advances(claim.employee) + advances = [entry.name for entry in advances] + self.assertTrue(advance.name in advances) + def test_repay_unclaimed_amount_from_salary(self): employee_name = make_employee("_T@employe.advance") advance = make_employee_advance(employee_name, {"repay_unclaimed_amount_from_salary": 1}) + pe = make_payment_entry(advance) + pe.submit() args = {"type": "Deduction"} create_salary_component("Advance Salary - Deduction", **args) @@ -85,11 +192,13 @@ def test_repay_unclaimed_amount_from_salary(self): advance.reload() self.assertEqual(advance.return_amount, 1000) + self.assertEqual(advance.status, "Returned") # update advance return amount on additional salary cancellation additional_salary.cancel() advance.reload() self.assertEqual(advance.return_amount, 700) + self.assertEqual(advance.status, "Paid") def tearDown(self): frappe.db.rollback() diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js index 047945787d7b..af80b63845e8 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim.js @@ -171,7 +171,7 @@ frappe.ui.form.on("Expense Claim", { ['docstatus', '=', 1], ['employee', '=', frm.doc.employee], ['paid_amount', '>', 0], - ['status', '!=', 'Claimed'] + ['status', 'not in', ['Claimed', 'Returned', 'Partly Claimed and Returned']] ] }; }); diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index 311a1eb81c80..89d86c1bc7ce 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -414,25 +414,27 @@ def get_expense_claim_account(expense_claim_type, company): @frappe.whitelist() def get_advances(employee, advance_id=None): + advance = frappe.qb.DocType("Employee Advance") + + query = frappe.qb.from_(advance).select( + advance.name, + advance.posting_date, + advance.paid_amount, + advance.claimed_amount, + advance.advance_account, + ) + if not advance_id: - condition = "docstatus=1 and employee={0} and paid_amount > 0 and paid_amount > claimed_amount + return_amount".format( - frappe.db.escape(employee) + query = query.where( + (advance.docstatus == 1) + & (advance.employee == employee) + & (advance.paid_amount > 0) + & (advance.status.notin(["Claimed", "Returned", "Partly Claimed and Returned"])) ) else: - condition = "name={0}".format(frappe.db.escape(advance_id)) - - return frappe.db.sql( - """ - select - name, posting_date, paid_amount, claimed_amount, advance_account - from - `tabEmployee Advance` - where {0} - """.format( - condition - ), - as_dict=1, - ) + query = query.where(advance.name == advance_id) + + return query.run(as_dict=True) @frappe.whitelist() diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.py b/erpnext/hr/doctype/leave_encashment/leave_encashment.py index 0f655e3e0fc4..7c0f0db19756 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.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 @@ def get_leave_details_for_encashment(self): 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 @@ def get_leave_details_for_encashment(self): 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 83eb969feb01..d06b6a3764df 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 @@ def setUp(self): 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 @@ def setUp(self): 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 @@ def test_leave_balance_value_and_amount(self): 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_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, + 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): - 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() diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 9a43c2aec63e..d3840bfb2e25 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -449,8 +449,6 @@ def make_gl_entries(self, cancel=0, adv_adj=0): "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/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 983b3b17e689..cc2f8c60e586 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 @@ def get_current_operation_data(self): }, ) - 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." + 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: + 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] + + 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(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 @@ -866,6 +897,7 @@ def set_missing_values(source, target): 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 4647ddf05f7c..b5371af2ccbd 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -2,14 +2,21 @@ # 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 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 ( + JobCardOverTransferError, + 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 @@ -17,34 +24,36 @@ class TestJobCard(FrappeTestCase): def setUp(self): make_bom_for_jc_tests() - - transfer_material_against, source_warehouse = None, None - - 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, - ) + self.transfer_material_against = "Work Order" + self.source_warehouse = None + self._work_order = None + + @property + def work_order(self) -> WorkOrder: + """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 + + 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 +67,6 @@ def test_job_card(self): 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 +102,11 @@ def test_job_card_with_different_work_station(self): ) 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 +135,10 @@ def test_job_card_overlap(self): 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) @@ -165,24 +163,24 @@ def test_job_card_multiple_materials_transfer(self): # 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) - 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 +193,7 @@ def test_job_card_excess_material_transfer(self): # 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( @@ -208,19 +206,41 @@ def test_job_card_excess_material_transfer(self): # 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): + + 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}) + + # 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" + 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 +252,7 @@ def test_job_card_partial_material_transfer(self): 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) @@ -277,7 +297,49 @@ def test_job_card_material_transfer_correctness(self): 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(): diff --git a/erpnext/non_profit/doctype/donation/donation.py b/erpnext/non_profit/doctype/donation/donation.py index 8e5ac5b61bf4..ed4b3d05b3ff 100644 --- a/erpnext/non_profit/doctype/donation/donation.py +++ b/erpnext/non_profit/doctype/donation/donation.py @@ -100,7 +100,9 @@ def capture_razorpay_donations(*args, **kwargs): return # to avoid capturing subscription payments as donations - if payment.description and "subscription" in str(payment.description).lower(): + if payment.invoice_id or ( + payment.description and "subscription" in str(payment.description).lower() + ): return donor = get_donor(payment.email) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index f29005a6d4b9..7f7abd065949 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -13,6 +13,7 @@ from frappe.utils import add_days, add_months, add_years, get_link_to_form, getdate, nowdate import erpnext +from erpnext import get_company_currency from erpnext.non_profit.doctype.member.member import create_member @@ -61,10 +62,6 @@ def validate_membership_period(self): frappe.throw(_("You can only renew if your membership expires within 30 days")) self.from_date = add_days(last_membership.to_date, 1) - elif frappe.session.user == "Administrator": - self.from_date = self.from_date - else: - self.from_date = nowdate() if frappe.db.get_single_value("Non Profit Settings", "billing_cycle") == "Yearly": self.to_date = add_years(self.from_date, 1) @@ -207,7 +204,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": [{"item_code": plan.linked_item, "rate": membership.amount, "qty": 1}], diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py index aef34a69606e..d73c2bed5f4a 100644 --- a/erpnext/non_profit/doctype/membership/test_membership.py +++ b/erpnext/non_profit/doctype/membership/test_membership.py @@ -94,7 +94,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, diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 1ff33c7f7bfb..85780501def6 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -366,3 +366,5 @@ erpnext.patches.v13_0.education_deprecation_warning erpnext.patches.v13_0.requeue_recoverable_reposts erpnext.patches.v13_0.create_accounting_dimensions_in_orders erpnext.patches.v13_0.set_per_billed_in_return_delivery_note +erpnext.patches.v13_0.update_employee_advance_status + diff --git a/erpnext/patches/v13_0/update_employee_advance_status.py b/erpnext/patches/v13_0/update_employee_advance_status.py new file mode 100644 index 000000000000..fc9e05e836d2 --- /dev/null +++ b/erpnext/patches/v13_0/update_employee_advance_status.py @@ -0,0 +1,29 @@ +import frappe + + +def execute(): + frappe.reload_doc("hr", "doctype", "employee_advance") + + advance = frappe.qb.DocType("Employee Advance") + ( + frappe.qb.update(advance) + .set(advance.status, "Returned") + .where( + (advance.docstatus == 1) + & ((advance.return_amount) & (advance.paid_amount == advance.return_amount)) + & (advance.status == "Paid") + ) + ).run() + + ( + frappe.qb.update(advance) + .set(advance.status, "Partly Claimed and Returned") + .where( + (advance.docstatus == 1) + & ( + (advance.claimed_amount & advance.return_amount) + & (advance.paid_amount == (advance.return_amount + advance.claimed_amount)) + ) + & (advance.status == "Paid") + ) + ).run() diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py index f57d9d37cf1e..18bd3b7733c0 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py @@ -124,6 +124,8 @@ def update_return_amount_in_employee_advance(self): return_amount += self.amount frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", return_amount) + advance = frappe.get_doc("Employee Advance", self.ref_docname) + advance.set_status(update=True) def update_employee_referral(self, cancel=False): if self.ref_doctype == "Employee Referral": diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 60d38f4ca492..5f2af74dca68 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -15,6 +15,7 @@ comma_and, date_diff, flt, + get_link_to_form, getdate, ) @@ -44,6 +45,7 @@ def on_submit(self): 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")) @@ -65,6 +67,14 @@ def validate_employee_details(self): 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", diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index 2570df70261f..4aeef81cbfbb 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", "read_only": 1 }, @@ -458,7 +458,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 4, - "modified": "2021-04-28 16:36:11.654632", + "modified": "2022-05-25 22:45:06.108499", "modified_by": "Administrator", "module": "Projects", "name": "Project", @@ -502,4 +502,4 @@ "timeline_field": "customer", "title_field": "project_name", "track_seen": 1 -} \ No newline at end of file +} diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index 57bfd5b60743..7298c037a709 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -84,7 +84,9 @@ def test_sales_invoice_from_timesheet(self): emp = make_employee("test_employee_6@salary.com") timesheet = make_timesheet(emp, simulate=True, is_billable=1) - sales_invoice = make_sales_invoice(timesheet.name, "_Test Item", "_Test Customer") + sales_invoice = make_sales_invoice( + timesheet.name, "_Test Item", "_Test Customer", currency="INR" + ) sales_invoice.due_date = nowdate() sales_invoice.submit() timesheet = frappe.get_doc("Timesheet", timesheet.name) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 2b1b0e3576b4..fe23ff381265 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -767,11 +767,23 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ 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); } diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index ea56d07d6dad..4748b265dc2b 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 75da981c2bcb..e20a915bb227 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -648,6 +648,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) @@ -764,7 +766,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, ) @@ -796,7 +800,8 @@ def __init__(self, doctype=None, docname=None): 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" @@ -1184,6 +1189,7 @@ def cancel_eway_bill(self, eway_bill, reason, remark): 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"): @@ -1357,9 +1363,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() diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js index 5f6dcdeb9227..88973e36b6ab 100644 --- a/erpnext/regional/india/taxes.js +++ b/erpnext/regional/india/taxes.js @@ -22,6 +22,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, diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 373e6e502ba0..602a71c3b8ec 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -449,7 +449,7 @@ def get_items_based_on_tax_rate(self): 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") 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 cb22fb6a80f6..91f4a5e50a5c 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"), diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 55c9ac47e4c8..3474ca0db683 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -24,7 +24,7 @@ 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 diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index de9af084abec..52011afefd18 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -299,19 +299,17 @@ def validate_item(self): 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( @@ -1139,7 +1137,7 @@ def _validate_work_order(pro_doc): 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) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index f824787226b8..8703aefdda7f 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 @@ -13,6 +11,7 @@ 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, ) @@ -1400,6 +1399,21 @@ def test_mapped_stock_entry(self): 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) diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py index a96ffefd474b..f2594f65fab9 100644 --- a/erpnext/stock/reorder_item.py +++ b/erpnext/stock/reorder_item.py @@ -253,11 +253,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") diff --git a/erpnext/templates/emails/request_for_quotation.html b/erpnext/templates/emails/request_for_quotation.html index 3283987fab05..5b073e604ff9 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 }}

- -{% endif %} diff --git a/erpnext/tests/test_search.py b/erpnext/tests/test_search.py index ffe9a5ae5410..3685828667ca 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"} diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 329fd3b1b9cb..545d0dde0447 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -1695,7 +1695,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., @@ -2021,7 +2021,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, @@ -2765,7 +2765,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, @@ -3703,7 +3703,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, @@ -3913,7 +3913,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", @@ -6235,7 +6235,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, @@ -6491,7 +6491,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.-, @@ -6790,7 +6790,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, @@ -7761,7 +7761,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, @@ -8937,7 +8937,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,