From da7b69109a4b3a5f5d8e1ac130008918e8a6763f Mon Sep 17 00:00:00 2001 From: Carlos9K Date: Sun, 18 Aug 2024 12:49:24 +0000 Subject: [PATCH 1/4] fix: getting scrap items from sub assemblies by fetching scrap items for parent BOM --- erpnext/manufacturing/doctype/bom/bom.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 009320c7a188..65e0fb609cac 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1511,7 +1511,10 @@ def get_scrap_items_from_sub_assemblies(bom_no, company, qty, scrap_items=None): fields=["bom_no", "qty"], order_by="idx asc", ) - + # fetch Scrap Items for Parent Bom + items = get_bom_items_as_dict(bom_no, company, qty=qty, fetch_exploded=0, fetch_scrap_items=1) + scrap_items.update(items) + for row in bom_items: if not row.bom_no: continue From b62df307d38247a59122efbade3ed9aba0d02b45 Mon Sep 17 00:00:00 2001 From: KerollesFathy Date: Mon, 26 Aug 2024 18:09:09 +0300 Subject: [PATCH 2/4] style: format code --- 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 65e0fb609cac..303670cee726 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1514,7 +1514,7 @@ def get_scrap_items_from_sub_assemblies(bom_no, company, qty, scrap_items=None): # fetch Scrap Items for Parent Bom items = get_bom_items_as_dict(bom_no, company, qty=qty, fetch_exploded=0, fetch_scrap_items=1) scrap_items.update(items) - + for row in bom_items: if not row.bom_no: continue From 5db66bd6dd2a6ab37f559f4d389e38f497f6649f Mon Sep 17 00:00:00 2001 From: KerollesFathy Date: Wed, 28 Aug 2024 12:59:18 +0300 Subject: [PATCH 3/4] test: get scrap items from sub assemblies consider the parent BOM --- erpnext/manufacturing/doctype/bom/test_bom.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index d02b51ca6e7a..e6ee59afb273 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -755,6 +755,19 @@ def test_do_not_include_manufacturing_and_fixed_items(self): self.assertTrue("_Test RM Item 2 Fixed Asset Item" not in items) self.assertTrue("_Test RM Item 3 Manufacture Item" in items) + def test_get_scrap_items_from_sub_assemblies(self): + from erpnext.manufacturing.doctype.bom.bom import get_scrap_items_from_sub_assemblies + + bom = frappe.copy_doc(test_records[1]) + bom.insert(ignore_mandatory=True) + + bom_scraped_items = [i.get("item_code") for i in bom.get("scrap_items", [])] + + # get scrapted items for parent bom + scraped_items = get_scrap_items_from_sub_assemblies(bom.name, bom.company, 2, None) + for item_code in scraped_items.keys(): + self.assertIn(item_code, bom_scraped_items, f"Item {item_code} not found in BOM scrap items") + def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) From 6a84f0b0272d9e9c39c583e652cc55823a68e690 Mon Sep 17 00:00:00 2001 From: KerollesFathy Date: Wed, 11 Dec 2024 13:41:03 +0200 Subject: [PATCH 4/4] chore: resync version-15-hotfix with feature branch --- CODEOWNERS | 16 +- erpnext/__init__.py | 13 +- erpnext/accounts/deferred_revenue.py | 2 +- erpnext/accounts/doctype/account/account.json | 4 +- erpnext/accounts/doctype/account/account.py | 38 +- .../in_standard_chart_of_accounts.json | 6 +- .../verified/standard_chart_of_accounts.py | 2 + .../account_closing_balance.py | 4 +- .../accounting_dimension.js | 2 +- .../accounting_dimension.py | 2 + .../accounting_dimension_filter.py | 5 +- .../accounting_period/accounting_period.py | 2 + .../advance_payment_ledger_entry/__init__.py | 0 .../advance_payment_ledger_entry.js | 8 + .../advance_payment_ledger_entry.json | 113 + .../advance_payment_ledger_entry.py | 27 + .../test_advance_payment_ledger_entry.py | 222 ++ .../doctype/bank_account/bank_account.json | 47 +- .../bank_account/bank_account_dashboard.py | 20 - .../doctype/bank_clearance/bank_clearance.js | 26 +- .../doctype/bank_clearance/bank_clearance.py | 25 +- .../bank_clearance/test_bank_clearance.py | 81 +- .../bank_transaction/bank_transaction.py | 18 +- .../test_cost_center_allocation.py | 53 +- .../currency_exchange_settings.py | 2 +- erpnext/accounts/doctype/dunning/dunning.json | 51 +- erpnext/accounts/doctype/dunning/dunning.py | 38 +- .../exchange_rate_revaluation.py | 82 +- .../test_exchange_rate_revaluation.py | 4 +- .../doctype/fiscal_year/fiscal_year.js | 5 +- .../doctype/fiscal_year/fiscal_year.json | 4 +- .../accounts/doctype/gl_entry/gl_entry.json | 106 +- erpnext/accounts/doctype/gl_entry/gl_entry.py | 3 +- .../doctype/journal_entry/journal_entry.js | 29 +- .../doctype/journal_entry/journal_entry.py | 20 +- .../journal_entry/test_journal_entry.py | 66 + .../opening_invoice_creation_tool.js | 16 +- .../doctype/payment_entry/payment_entry.js | 487 +-- .../doctype/payment_entry/payment_entry.json | 8 +- .../doctype/payment_entry/payment_entry.py | 822 ++++- .../payment_entry/test_payment_entry.py | 158 +- .../payment_entry_deduction.json | 11 +- .../payment_entry_deduction.py | 1 + .../payment_entry_reference.json | 28 +- .../payment_entry_reference.py | 12 +- .../payment_reconciliation.py | 27 +- .../test_payment_reconciliation.py | 265 +- .../payment_reconciliation_payment.json | 16 +- .../payment_reconciliation_payment.py | 2 +- .../payment_request/payment_request.js | 4 +- .../payment_request/payment_request.json | 52 +- .../payment_request/payment_request.py | 423 ++- .../payment_request_dashboard.py | 14 + .../payment_request/payment_request_list.js | 27 +- .../payment_request/test_payment_request.py | 294 +- .../period_closing_voucher.js | 18 + .../period_closing_voucher.json | 36 +- .../period_closing_voucher.py | 647 ++-- .../test_period_closing_voucher.py | 8 +- .../pos_closing_entry/pos_closing_entry.js | 10 +- .../pos_closing_entry/pos_closing_entry.py | 10 +- .../doctype/pos_invoice/pos_invoice.js | 16 +- .../doctype/pos_invoice/pos_invoice.json | 10 +- .../doctype/pos_invoice/pos_invoice.py | 42 +- .../doctype/pos_invoice/test_pos_invoice.py | 2 +- .../pos_invoice_merge_log.py | 15 +- .../test_pos_invoice_merge_log.py | 2 +- .../doctype/pricing_rule/pricing_rule.json | 7 +- .../doctype/pricing_rule/pricing_rule.py | 23 +- .../doctype/pricing_rule/test_pricing_rule.py | 48 +- .../accounts/doctype/pricing_rule/utils.py | 33 +- .../process_payment_reconciliation.js | 21 +- .../process_payment_reconciliation.json | 16 +- .../process_payment_reconciliation.py | 2 + .../process_statement_of_accounts.json | 9 +- .../process_statement_of_accounts.py | 3 + .../promotional_scheme/promotional_scheme.py | 52 +- .../test_promotional_scheme.py | 25 + .../purchase_invoice/purchase_invoice.js | 71 +- .../purchase_invoice/purchase_invoice.json | 5 +- .../purchase_invoice/purchase_invoice.py | 155 +- .../purchase_invoice/test_purchase_invoice.py | 212 ++ .../purchase_invoice_item.json | 5 +- .../repost_accounting_ledger.py | 6 +- .../test_repost_accounting_ledger.py | 6 +- .../doctype/sales_invoice/sales_invoice.js | 17 +- .../doctype/sales_invoice/sales_invoice.json | 14 +- .../doctype/sales_invoice/sales_invoice.py | 79 +- .../sales_invoice/sales_invoice_list.js | 2 +- .../sales_invoice/test_sales_invoice.py | 420 ++- .../sales_invoice_item.json | 31 +- .../sales_invoice_item/sales_invoice_item.py | 1 + .../doctype/subscription/subscription.py | 5 +- erpnext/accounts/doctype/tax_rule/tax_rule.py | 2 +- .../tax_withholding_category.py | 42 +- .../test_tax_withholding_category.py | 129 +- .../test_unreconcile_payment.py | 107 +- .../unreconcile_payment.json | 5 +- erpnext/accounts/general_ledger.py | 238 +- .../total_incoming_bills.json | 3 +- .../total_incoming_payment.json | 1 + .../total_outgoing_bills.json | 1 + .../total_outgoing_payment.json | 1 + erpnext/accounts/party.py | 60 +- .../accounts_payable/accounts_payable.js | 35 +- .../accounts_payable/test_accounts_payable.py | 5 +- .../accounts_payable_summary.js | 30 +- .../accounts_receivable.js | 30 +- .../accounts_receivable.py | 174 +- .../test_accounts_receivable.py | 95 +- .../accounts_receivable_summary.js | 30 +- .../accounts_receivable_summary.py | 54 +- .../test_accounts_receivable_summary.py | 10 +- .../asset_depreciation_ledger.py | 4 +- .../asset_depreciations_and_balances.js | 6 + .../asset_depreciations_and_balances.py | 160 +- .../report/balance_sheet/balance_sheet.py | 26 +- .../bank_reconciliation_statement.js | 16 + .../bank_reconciliation_statement.py | 4 +- .../accounts/report/cash_flow/cash_flow.js | 5 +- .../accounts/report/cash_flow/cash_flow.py | 64 +- .../__init__.py | 0 ...heques_and_deposits_incorrectly_cleared.js | 44 + ...ques_and_deposits_incorrectly_cleared.json | 29 + ...heques_and_deposits_incorrectly_cleared.py | 153 + .../consolidated_financial_statement.py | 72 +- .../deferred_revenue_and_expense.py | 33 +- .../accounts/report/financial_statements.py | 216 +- .../general_and_payment_ledger_comparison.py | 6 +- .../report/general_ledger/general_ledger.html | 5 +- .../report/general_ledger/general_ledger.py | 21 +- .../report/gross_profit/gross_profit.py | 135 +- .../report/gross_profit/test_gross_profit.py | 8 +- .../report/invalid_ledger_entries/__init__.py | 0 .../invalid_ledger_entries.js | 51 + .../invalid_ledger_entries.json | 23 + .../invalid_ledger_entries.py | 137 + .../report/payment_ledger/payment_ledger.py | 5 +- .../profit_and_loss_statement.py | 16 +- .../purchase_register/purchase_register.py | 5 +- .../report/sales_register/sales_register.py | 3 +- .../sales_register/test_sales_register.py | 179 + .../tax_withholding_details.py | 2 +- .../report/trial_balance/trial_balance.py | 20 +- erpnext/accounts/report/utils.py | 9 +- erpnext/accounts/test/accounts_mixin.py | 38 + erpnext/accounts/test/test_reports.py | 4 +- erpnext/accounts/test/test_utils.py | 8 +- erpnext/accounts/utils.py | 179 +- erpnext/assets/doctype/asset/asset.js | 12 +- erpnext/assets/doctype/asset/asset.json | 24 +- erpnext/assets/doctype/asset/asset.py | 89 +- .../asset_capitalization.py | 30 +- .../test_asset_capitalization.py | 21 +- .../asset_depreciation_schedule.py | 6 +- .../asset_maintenance/asset_maintenance.py | 1 + .../asset_maintenance_log.json | 12 +- .../asset_maintenance_log.py | 1 + .../number_card/asset_value/asset_value.json | 1 + .../new_assets_(this_year).json | 1 + .../total_assets/total_assets.json | 1 + .../doctype/purchase_order/purchase_order.js | 55 +- .../doctype/purchase_order/purchase_order.py | 53 +- .../purchase_order_dashboard.py | 13 +- .../purchase_order/purchase_order_list.js | 17 +- .../request_for_quotation.js | 11 +- .../request_for_quotation.py | 2 + erpnext/buying/doctype/supplier/supplier.py | 2 +- .../item_wise_purchase_history.js | 62 + .../item_wise_purchase_history.json | 40 +- .../item_wise_purchase_history.py | 276 ++ .../procurement_tracker.py | 2 +- .../purchase_order_analysis.py | 37 +- .../purchase_order_trends.js | 7 + erpnext/controllers/accounts_controller.py | 284 +- erpnext/controllers/buying_controller.py | 22 +- erpnext/controllers/item_variant.py | 2 +- erpnext/controllers/print_settings.py | 8 +- erpnext/controllers/queries.py | 12 +- .../controllers/sales_and_purchase_return.py | 76 +- erpnext/controllers/selling_controller.py | 81 +- erpnext/controllers/stock_controller.py | 68 +- .../controllers/subcontracting_controller.py | 94 +- erpnext/controllers/taxes_and_totals.py | 32 +- .../tests/test_accounts_controller.py | 26 + .../tests/test_subcontracting_controller.py | 3 + erpnext/controllers/trends.py | 10 +- erpnext/crm/doctype/lead/lead.js | 169 +- .../crm/doctype/opportunity/opportunity.json | 7 +- erpnext/crm/doctype/prospect/test_prospect.py | 23 + erpnext/crm/frappe_crm_api.py | 173 + .../open_opportunity/open_opportunity.json | 2 +- erpnext/edi/__init__.py | 0 erpnext/edi/doctype/__init__.py | 0 erpnext/edi/doctype/code_list/__init__.py | 0 erpnext/edi/doctype/code_list/code_list.js | 51 + erpnext/edi/doctype/code_list/code_list.json | 112 + erpnext/edi/doctype/code_list/code_list.py | 125 + .../edi/doctype/code_list/code_list_import.js | 218 ++ .../edi/doctype/code_list/code_list_import.py | 140 + .../edi/doctype/code_list/code_list_list.js | 8 + .../edi/doctype/code_list/test_code_list.py | 9 + erpnext/edi/doctype/common_code/__init__.py | 0 .../edi/doctype/common_code/common_code.js | 8 + .../edi/doctype/common_code/common_code.json | 103 + .../edi/doctype/common_code/common_code.py | 114 + .../doctype/common_code/common_code_list.js | 8 + .../doctype/common_code/test_common_code.py | 9 + .../doctype/plaid_settings/plaid_settings.py | 2 +- erpnext/hooks.py | 21 +- erpnext/manufacturing/doctype/bom/bom.js | 81 +- erpnext/manufacturing/doctype/bom/bom.py | 78 +- erpnext/manufacturing/doctype/bom/test_bom.py | 20 + .../doctype/bom_creator/bom_creator.json | 3 +- .../doctype/bom_update_log/bom_update_log.py | 8 +- .../doctype/job_card/job_card.js | 2 +- .../doctype/job_card/job_card.py | 25 +- .../manufacturing_settings.json | 59 +- .../manufacturing_settings.py | 9 +- .../doctype/plant_floor/plant_floor.js | 21 + .../doctype/plant_floor/plant_floor.json | 5 +- .../production_plan/production_plan.js | 22 +- .../production_plan/production_plan.json | 8 +- .../production_plan/production_plan.py | 88 +- .../doctype/work_order/test_work_order.py | 317 ++ .../doctype/work_order/work_order.js | 85 +- .../doctype/work_order/work_order.py | 59 +- .../work_order_item/work_order_item.json | 13 +- .../work_order_item/work_order_item.py | 2 + .../monthly_completed_work_order.json | 1 + .../monthly_total_work_order.json | 1 + .../ongoing_job_card/ongoing_job_card.json | 1 + .../production_analytics.py | 10 +- .../production_plan_summary.py | 51 +- .../quality_inspection_summary.py | 4 +- .../manufacturing/manufacturing.json | 157 +- erpnext/modules.txt | 1 + erpnext/patches.txt | 16 +- .../fix_additional_cost_in_mfg_stock_entry.py | 2 +- .../item_reposting_for_incorrect_sl_and_gl.py | 3 +- .../v14_0/set_period_start_end_date_in_pcv.py | 17 + .../patches/v14_0/single_to_multi_dunning.py | 2 +- .../patches/v14_0/update_closing_balances.py | 120 +- ...rency_exchange_settings_for_frankfurter.py | 11 + .../v14_0/update_reports_with_range.py | 39 + .../update_stock_uom_in_work_order_item.py | 15 + .../add_disassembly_order_stock_entry_type.py | 13 + .../create_advance_payment_ledger_records.py | 73 + .../drop_index_posting_datetime_from_sle.py | 16 + .../v15_0/enable_allow_existing_serial_no.py | 6 + .../v15_0/link_purchase_item_to_asset_doc.py | 74 + ...e_gain_loss_in_payment_entry_deductions.py | 22 + .../v15_0/set_standard_stock_entry_type.py | 17 + .../patches/v15_0/update_invoice_remarks.py | 149 + .../update_sub_voucher_type_in_gl_entries.py | 57 + ...ee_email_field_in_asset_maintenance_log.py | 18 + erpnext/projects/doctype/project/project.js | 1 + erpnext/projects/doctype/project/project.py | 49 +- .../projects/doctype/project/test_project.py | 28 + erpnext/projects/doctype/task/task.py | 4 +- .../projects/doctype/timesheet/timesheet.js | 4 +- .../projects/doctype/timesheet/timesheet.py | 48 +- .../delayed_tasks_summary.py | 2 +- .../report/project_summary/project_summary.py | 8 +- erpnext/public/images/erpnext-logo-blue.png | Bin 0 -> 4235 bytes .../dialog_manager.js | 8 + .../bom_configurator.bundle.js | 12 +- erpnext/public/js/controllers/accounts.js | 9 +- erpnext/public/js/controllers/buying.js | 22 +- .../public/js/controllers/taxes_and_totals.js | 47 +- erpnext/public/js/controllers/transaction.js | 139 +- erpnext/public/js/financial_statements.js | 61 +- erpnext/public/js/queries.js | 17 +- .../visual_plant_floor_template.html | 20 +- erpnext/public/js/utils.js | 1 + erpnext/public/js/utils/sales_common.js | 6 +- .../js/utils/serial_no_batch_selector.js | 216 +- erpnext/public/js/utils/unreconcile.js | 9 +- erpnext/public/scss/erpnext.scss | 41 + .../selling/doctype/customer/customer.json | 19 +- erpnext/selling/doctype/customer/customer.py | 3 +- .../doctype/product_bundle/product_bundle.py | 14 +- .../selling/doctype/quotation/quotation.js | 16 +- .../selling/doctype/quotation/quotation.json | 21 +- .../selling/doctype/quotation/quotation.py | 147 +- .../doctype/quotation/test_quotation.py | 56 +- .../quotation_item/quotation_item.json | 31 +- .../doctype/quotation_item/quotation_item.py | 1 + .../doctype/sales_order/sales_order.js | 52 +- .../doctype/sales_order/sales_order.json | 12 +- .../doctype/sales_order/sales_order.py | 38 +- .../sales_order/sales_order_calendar.js | 1 + .../sales_order/sales_order_dashboard.py | 4 +- .../doctype/sales_order/sales_order_list.js | 10 +- .../doctype/sales_order/test_sales_order.py | 7 + .../sales_order_item/sales_order_item.json | 29 +- .../sales_order_item/sales_order_item.py | 1 + .../active_customers/active_customers.json | 1 - .../page/point_of_sale/point_of_sale.py | 18 +- .../page/point_of_sale/pos_controller.js | 12 +- .../page/point_of_sale/pos_item_cart.js | 49 +- .../page/point_of_sale/pos_item_details.js | 2 +- .../page/point_of_sale/pos_item_selector.js | 4 +- .../point_of_sale/pos_past_order_summary.js | 6 +- .../selling/page/point_of_sale/pos_payment.js | 7 +- .../payment_terms_status_for_sales_order.py | 4 +- .../report/sales_analytics/sales_analytics.js | 5 + .../report/sales_analytics/sales_analytics.py | 112 +- .../sales_order_analysis.py | 2 +- .../sales_order_trends/sales_order_trends.js | 7 + erpnext/setup/default_success_action.py | 5 +- erpnext/setup/doctype/brand/brand.js | 56 +- erpnext/setup/doctype/brand/brand.json | 5 +- erpnext/setup/doctype/company/company.js | 4 +- erpnext/setup/doctype/company/company.json | 58 +- erpnext/setup/doctype/company/company.py | 7 +- .../currency_exchange/currency_exchange.js | 34 +- .../test_currency_exchange.py | 4 +- .../doctype/customer_group/customer_group.py | 5 - .../doctype/holiday_list/holiday_list.py | 6 +- .../transaction_deletion_record.py | 16 +- erpnext/setup/install.py | 2 +- .../setup_wizard/data/country_wise_tax.json | 361 +- .../operations/install_fixtures.py | 33 +- .../setup_wizard/operations/taxes_setup.py | 8 +- erpnext/stock/dashboard/item_dashboard.js | 9 +- erpnext/stock/dashboard/item_dashboard.py | 1 + .../stock/dashboard/item_dashboard_list.html | 2 + .../oldest_items/oldest_items.json | 9 +- erpnext/stock/doctype/batch/batch.py | 4 +- .../doctype/delivery_note/delivery_note.json | 12 +- .../doctype/delivery_note/delivery_note.py | 96 +- .../delivery_note/test_delivery_note.py | 303 ++ .../delivery_note_item.json | 32 +- .../delivery_note_item/delivery_note_item.py | 1 + .../inventory_dimension.py | 39 +- .../test_inventory_dimension.py | 53 +- erpnext/stock/doctype/item/item.js | 105 +- erpnext/stock/doctype/item/item.py | 79 +- .../doctype/item/templates/item_row.html | 2 +- .../item_attribute/item_attribute.json | 17 +- .../doctype/item_attribute/item_attribute.py | 14 + .../item_variant_attribute.json | 12 +- .../item_variant_attribute.py | 1 + .../material_request/material_request.js | 23 +- .../material_request/material_request.py | 13 +- erpnext/stock/doctype/pick_list/pick_list.py | 4 +- .../stock/doctype/pick_list/pick_list_list.js | 2 +- .../purchase_receipt/purchase_receipt.js | 43 +- .../purchase_receipt/purchase_receipt.json | 4 +- .../purchase_receipt/purchase_receipt.py | 49 +- .../purchase_receipt/test_purchase_receipt.py | 394 ++- .../quality_inspection/quality_inspection.js | 2 +- .../quality_inspection/quality_inspection.py | 26 +- .../repost_item_valuation.py | 2 +- .../serial_and_batch_bundle.py | 255 +- .../stock/doctype/stock_entry/stock_entry.js | 21 +- .../doctype/stock_entry/stock_entry.json | 20 +- .../stock/doctype/stock_entry/stock_entry.py | 116 +- .../doctype/stock_entry/test_stock_entry.py | 165 +- .../stock_entry_type/stock_entry_type.json | 15 +- .../stock_entry_type/stock_entry_type.py | 19 +- .../stock_entry_type/test_stock_entry_type.py | 29 +- .../stock_ledger_entry.json | 5 +- .../stock_ledger_entry/stock_ledger_entry.py | 72 +- .../test_stock_ledger_entry.py | 7 +- .../stock_reconciliation.py | 68 +- .../test_stock_reconciliation.py | 54 + .../stock_reservation_entry.py | 5 +- .../stock_settings/stock_settings.json | 11 +- .../doctype/stock_settings/stock_settings.py | 9 +- erpnext/stock/get_item_details.py | 44 +- .../total_warehouses/total_warehouses.json | 1 + erpnext/stock/reorder_item.py | 9 +- .../available_batch_report.js | 2 +- .../batch_wise_balance_history.py | 2 +- .../serial_and_batch_summary.py | 2 - .../serial_no_service_contract_expiry.json | 43 +- .../serial_no_status/serial_no_status.json | 43 +- .../serial_no_warranty_expiry.json | 43 +- .../stock/report/stock_ageing/stock_ageing.js | 23 +- .../stock/report/stock_ageing/stock_ageing.py | 41 +- .../report/stock_ageing/test_stock_ageing.py | 4 +- .../report/stock_balance/stock_balance.js | 6 + .../report/stock_balance/stock_balance.py | 5 +- .../stock_ledger_invariant_check.js | 6 + .../stock_ledger_invariant_check.py | 31 +- .../stock_ledger_variance.js | 18 +- .../stock_ledger_variance.py | 18 +- .../stock_projected_qty.py | 1 + erpnext/stock/report/test_reports.py | 5 +- ...rehouse_wise_item_balance_age_and_value.js | 15 + ...rehouse_wise_item_balance_age_and_value.py | 2 - erpnext/stock/serial_batch_bundle.py | 50 +- erpnext/stock/stock_ledger.py | 75 +- .../subcontracting_order.json | 21 +- .../subcontracting_order_list.js | 2 +- .../test_subcontracting_order.py | 23 + .../subcontracting_order_item.json | 11 +- .../subcontracting_order_item.py | 3 + .../subcontracting_receipt.js | 12 +- .../subcontracting_receipt.json | 27 +- .../subcontracting_receipt.py | 183 +- .../subcontracting_receipt_list.js | 2 +- .../test_subcontracting_receipt.py | 103 +- .../subcontracting_receipt_item.json | 9 +- .../subcontracting_receipt_item.py | 1 + .../service_level_agreement.py | 2 +- erpnext/templates/includes/issue_row.html | 2 +- .../includes/projects/project_row.html | 2 +- .../templates/includes/transaction_row.html | 2 +- erpnext/templates/pages/order.html | 4 +- erpnext/templates/pages/order.py | 10 +- erpnext/templates/pages/projects.py | 2 +- erpnext/templates/pages/timelog_info.html | 2 +- erpnext/tests/test_perf.py | 2 +- erpnext/translations/de.csv | 301 +- erpnext/translations/es.csv | 2525 ++++++++++++++ erpnext/translations/fa.csv | 2532 ++++++++++++++ erpnext/translations/sv.csv | 3083 +++++++++++++++++ erpnext/utilities/bulk_transaction.py | 5 + erpnext/utilities/transaction_base.py | 10 +- pyproject.toml | 2 +- 423 files changed, 23038 insertions(+), 3837 deletions(-) create mode 100644 erpnext/accounts/doctype/advance_payment_ledger_entry/__init__.py create mode 100644 erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.js create mode 100644 erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json create mode 100644 erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.py create mode 100644 erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py delete mode 100644 erpnext/accounts/doctype/bank_account/bank_account_dashboard.py create mode 100644 erpnext/accounts/doctype/payment_request/payment_request_dashboard.py create mode 100644 erpnext/accounts/report/cheques_and_deposits_incorrectly_cleared/__init__.py create mode 100644 erpnext/accounts/report/cheques_and_deposits_incorrectly_cleared/cheques_and_deposits_incorrectly_cleared.js create mode 100644 erpnext/accounts/report/cheques_and_deposits_incorrectly_cleared/cheques_and_deposits_incorrectly_cleared.json create mode 100644 erpnext/accounts/report/cheques_and_deposits_incorrectly_cleared/cheques_and_deposits_incorrectly_cleared.py create mode 100644 erpnext/accounts/report/invalid_ledger_entries/__init__.py create mode 100644 erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js create mode 100644 erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.json create mode 100644 erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py create mode 100644 erpnext/accounts/report/sales_register/test_sales_register.py create mode 100644 erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.js create mode 100644 erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.py create mode 100644 erpnext/crm/frappe_crm_api.py create mode 100644 erpnext/edi/__init__.py create mode 100644 erpnext/edi/doctype/__init__.py create mode 100644 erpnext/edi/doctype/code_list/__init__.py create mode 100644 erpnext/edi/doctype/code_list/code_list.js create mode 100644 erpnext/edi/doctype/code_list/code_list.json create mode 100644 erpnext/edi/doctype/code_list/code_list.py create mode 100644 erpnext/edi/doctype/code_list/code_list_import.js create mode 100644 erpnext/edi/doctype/code_list/code_list_import.py create mode 100644 erpnext/edi/doctype/code_list/code_list_list.js create mode 100644 erpnext/edi/doctype/code_list/test_code_list.py create mode 100644 erpnext/edi/doctype/common_code/__init__.py create mode 100644 erpnext/edi/doctype/common_code/common_code.js create mode 100644 erpnext/edi/doctype/common_code/common_code.json create mode 100644 erpnext/edi/doctype/common_code/common_code.py create mode 100644 erpnext/edi/doctype/common_code/common_code_list.js create mode 100644 erpnext/edi/doctype/common_code/test_common_code.py create mode 100644 erpnext/patches/v14_0/set_period_start_end_date_in_pcv.py create mode 100644 erpnext/patches/v14_0/update_currency_exchange_settings_for_frankfurter.py create mode 100644 erpnext/patches/v14_0/update_reports_with_range.py create mode 100644 erpnext/patches/v14_0/update_stock_uom_in_work_order_item.py create mode 100644 erpnext/patches/v15_0/add_disassembly_order_stock_entry_type.py create mode 100644 erpnext/patches/v15_0/create_advance_payment_ledger_records.py create mode 100644 erpnext/patches/v15_0/drop_index_posting_datetime_from_sle.py create mode 100644 erpnext/patches/v15_0/enable_allow_existing_serial_no.py create mode 100644 erpnext/patches/v15_0/link_purchase_item_to_asset_doc.py create mode 100644 erpnext/patches/v15_0/set_is_exchange_gain_loss_in_payment_entry_deductions.py create mode 100644 erpnext/patches/v15_0/set_standard_stock_entry_type.py create mode 100644 erpnext/patches/v15_0/update_invoice_remarks.py create mode 100644 erpnext/patches/v15_0/update_sub_voucher_type_in_gl_entries.py create mode 100644 erpnext/patches/v15_0/update_task_assignee_email_field_in_asset_maintenance_log.py create mode 100644 erpnext/public/images/erpnext-logo-blue.png diff --git a/CODEOWNERS b/CODEOWNERS index 9077c6783c7f..4a19fc871b5d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -4,21 +4,21 @@ # the repo. Unless a later match takes precedence, erpnext/accounts/ @deepeshgarg007 @ruthra-kumar -erpnext/assets/ @anandbaburajan @deepeshgarg007 +erpnext/assets/ @khushi8112 @deepeshgarg007 erpnext/regional @deepeshgarg007 @ruthra-kumar erpnext/selling @deepeshgarg007 @ruthra-kumar erpnext/support/ @deepeshgarg007 pos* -erpnext/buying/ @rohitwaghchaure @s-aga-r -erpnext/maintenance/ @rohitwaghchaure @s-aga-r -erpnext/manufacturing/ @rohitwaghchaure @s-aga-r -erpnext/quality_management/ @rohitwaghchaure @s-aga-r -erpnext/stock/ @rohitwaghchaure @s-aga-r -erpnext/subcontracting @rohitwaghchaure @s-aga-r +erpnext/buying/ @rohitwaghchaure +erpnext/maintenance/ @rohitwaghchaure +erpnext/manufacturing/ @rohitwaghchaure +erpnext/quality_management/ @rohitwaghchaure +erpnext/stock/ @rohitwaghchaure +erpnext/subcontracting @rohitwaghchaure erpnext/controllers/ @deepeshgarg007 @rohitwaghchaure erpnext/patches/ @deepeshgarg007 .github/ @deepeshgarg007 -pyproject.toml @phot0n +pyproject.toml @akhilnarang diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 4a9236066cce..bb8bf11b7ea0 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -2,8 +2,9 @@ import inspect import frappe +from frappe.utils.user import is_website_user -__version__ = "15.28.2" +__version__ = "15.45.4" def get_default_company(user=None): @@ -149,3 +150,13 @@ def caller(*args, **kwargs): return frappe.get_attr(overrides[function_path][-1])(*args, **kwargs) return caller + + +def check_app_permission(): + if frappe.session.user == "Administrator": + return True + + if is_website_user(): + return False + + return True diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index a48ce9b4c637..a88764cf1b2f 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -58,7 +58,7 @@ def build_conditions(process_type, account, company): ) if account: - conditions += f"AND {deferred_account}='{account}'" + conditions += f"AND {deferred_account}={frappe.db.escape(account)}" elif company: conditions += f"AND p.company = {frappe.db.escape(company)}" diff --git a/erpnext/accounts/doctype/account/account.json b/erpnext/accounts/doctype/account/account.json index e87b59ea9cba..7b56444e6356 100644 --- a/erpnext/accounts/doctype/account/account.json +++ b/erpnext/accounts/doctype/account/account.json @@ -121,7 +121,7 @@ "label": "Account Type", "oldfieldname": "account_type", "oldfieldtype": "Select", - "options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary", + "options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nRound Off for Opening\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary", "search_index": 1 }, { @@ -191,7 +191,7 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2024-06-27 16:23:04.444354", + "modified": "2024-08-19 15:19:11.095045", "modified_by": "Administrator", "module": "Accounts", "name": "Account", diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index 69c1e16bb1ee..b510651e68f6 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -60,6 +60,7 @@ class Account(NestedSet): "Payable", "Receivable", "Round Off", + "Round Off for Opening", "Stock", "Stock Adjustment", "Stock Received But Not Billed", @@ -103,14 +104,12 @@ def autoname(self): self.name = get_autoname_with_number(self.account_number, self.account_name, self.company) def validate(self): - from erpnext.accounts.utils import validate_field_number - if frappe.local.flags.allow_unverified_charts: return self.validate_parent() self.validate_parent_child_account_type() self.validate_root_details() - validate_field_number("Account", self.name, self.account_number, self.company, "account_number") + self.validate_account_number() self.validate_group_or_ledger() self.set_root_and_report_type() self.validate_mandatory() @@ -202,7 +201,7 @@ def validate_receivable_payable_account_type(self): msg = _( "There are ledger entries against this account. Changing {0} to non-{1} in live system will cause incorrect output in 'Accounts {2}' report" ).format( - frappe.bold("Account Type"), doc_before_save.account_type, doc_before_save.account_type + frappe.bold(_("Account Type")), doc_before_save.account_type, doc_before_save.account_type ) frappe.msgprint(msg) self.add_comment("Comment", msg) @@ -311,6 +310,22 @@ def validate_account_currency(self): if frappe.db.get_value("GL Entry", {"account": self.name}): frappe.throw(_("Currency can not be changed after making entries using some other currency")) + def validate_account_number(self, account_number=None): + if not account_number: + account_number = self.account_number + + if account_number: + account_with_same_number = frappe.db.get_value( + "Account", + {"account_number": account_number, "company": self.company, "name": ["!=", self.name]}, + ) + if account_with_same_number: + frappe.throw( + _("Account Number {0} already used in account {1}").format( + account_number, account_with_same_number + ) + ) + def create_account_for_child_company(self, parent_acc_name_map, descendants, parent_acc_name): for company in descendants: company_bold = frappe.bold(company) @@ -464,19 +479,6 @@ def get_account_autoname(account_number, account_name, company): return " - ".join(parts) -def validate_account_number(name, account_number, company): - if account_number: - account_with_same_number = frappe.db.get_value( - "Account", {"account_number": account_number, "company": company, "name": ["!=", name]} - ) - if account_with_same_number: - frappe.throw( - _("Account Number {0} already used in account {1}").format( - account_number, account_with_same_number - ) - ) - - @frappe.whitelist() def update_account_number(name, account_name, account_number=None, from_descendant=False): account = frappe.get_cached_doc("Account", name) @@ -517,7 +519,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda frappe.throw(message, title=_("Rename Not Allowed")) - validate_account_number(name, account_number, account.company) + account.validate_account_number(account_number) if account_number: frappe.db.set_value("Account", name, "account_number", account_number.strip()) else: diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/in_standard_chart_of_accounts.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/in_standard_chart_of_accounts.json index 2ec0b7f70c88..4d807b09c330 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/in_standard_chart_of_accounts.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/in_standard_chart_of_accounts.json @@ -109,7 +109,8 @@ "Utility Expenses": {}, "Write Off": {}, "Exchange Gain/Loss": {}, - "Gain/Loss on Asset Disposal": {} + "Gain/Loss on Asset Disposal": {}, + "Impairment": {} }, "root_type": "Expense" }, @@ -132,7 +133,8 @@ "Source of Funds (Liabilities)": { "Capital Account": { "Reserves and Surplus": {}, - "Shareholders Funds": {} + "Shareholders Funds": {}, + "Revaluation Surplus": {} }, "Current Liabilities": { "Accounts Payable": { diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py index e30ad24a374a..5a5e232db8d2 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py @@ -72,6 +72,7 @@ def get(): _("Write Off"): {}, _("Exchange Gain/Loss"): {}, _("Gain/Loss on Asset Disposal"): {}, + _("Impairment"): {}, }, "root_type": "Expense", }, @@ -104,6 +105,7 @@ def get(): _("Dividends Paid"): {"account_type": "Equity"}, _("Opening Balance Equity"): {"account_type": "Equity"}, _("Retained Earnings"): {"account_type": "Equity"}, + _("Revaluation Surplus"): {"account_type": "Equity"}, "root_type": "Equity", }, } diff --git a/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py index 82821e140eaf..6d5e023f039e 100644 --- a/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py +++ b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py @@ -113,9 +113,9 @@ def get_previous_closing_entries(company, closing_date, accounting_dimensions): entries = [] last_period_closing_voucher = frappe.db.get_all( "Period Closing Voucher", - filters={"docstatus": 1, "company": company, "posting_date": ("<", closing_date)}, + filters={"docstatus": 1, "company": company, "period_end_date": ("<", closing_date)}, fields=["name"], - order_by="posting_date desc", + order_by="period_end_date desc", limit=1, ) diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js index 4e45dede1d5a..6f4f9f8d782e 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js @@ -58,7 +58,7 @@ frappe.ui.form.on("Accounting Dimension", { }, label: function (frm) { - frm.set_value("fieldname", frappe.model.scrub(frm.doc.label)); + frm.set_value("fieldname", frm.doc.label.replace(/ /g, "_").replace(/-/g, "_").toLowerCase()); }, document_type: function (frm) { diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index db99bcd223b8..8fc22dd76506 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -7,6 +7,7 @@ import frappe from frappe import _, scrub from frappe.custom.doctype.custom_field.custom_field import create_custom_field +from frappe.database.schema import validate_column_name from frappe.model import core_doctypes_list from frappe.model.document import Document from frappe.utils import cstr @@ -60,6 +61,7 @@ def validate(self): if not self.is_new(): self.validate_document_type_change() + validate_column_name(self.fieldname) self.validate_dimension_defaults() def validate_document_type_change(self): diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py index 1954b4b0efe1..7c843cf552ef 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py @@ -74,12 +74,12 @@ def get_dimension_filter_map(): a.applicable_on_account, d.dimension_value, p.accounting_dimension, p.allow_or_restrict, a.is_mandatory FROM - `tabApplicable On Account` a, `tabAllowed Dimension` d, + `tabApplicable On Account` a, `tabAccounting Dimension Filter` p + LEFT JOIN `tabAllowed Dimension` d ON d.parent = p.name WHERE p.name = a.parent AND p.disabled = 0 - AND p.name = d.parent """, as_dict=1, ) @@ -97,7 +97,6 @@ def get_dimension_filter_map(): f.allow_or_restrict, f.is_mandatory, ) - frappe.flags.dimension_filter_map = dimension_filter_map return frappe.flags.dimension_filter_map diff --git a/erpnext/accounts/doctype/accounting_period/accounting_period.py b/erpnext/accounts/doctype/accounting_period/accounting_period.py index 172ef93f14d3..300d216618e6 100644 --- a/erpnext/accounts/doctype/accounting_period/accounting_period.py +++ b/erpnext/accounts/doctype/accounting_period/accounting_period.py @@ -101,6 +101,8 @@ def validate_accounting_period_on_doc_save(doc, method=None): date = doc.available_for_use_date elif doc.doctype == "Asset Repair": date = doc.completion_date + elif doc.doctype == "Period Closing Voucher": + date = doc.period_end_date else: date = doc.posting_date diff --git a/erpnext/accounts/doctype/advance_payment_ledger_entry/__init__.py b/erpnext/accounts/doctype/advance_payment_ledger_entry/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.js b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.js new file mode 100644 index 000000000000..1a0dc1e72721 --- /dev/null +++ b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.js @@ -0,0 +1,8 @@ +// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Advance Payment Ledger Entry", { +// refresh(frm) { + +// }, +// }); diff --git a/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json new file mode 100644 index 000000000000..290ed11c98ee --- /dev/null +++ b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json @@ -0,0 +1,113 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-10-16 16:57:12.085072", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "company", + "voucher_type", + "voucher_no", + "against_voucher_type", + "against_voucher_no", + "amount", + "currency", + "event" + ], + "fields": [ + { + "fieldname": "voucher_type", + "fieldtype": "Link", + "label": "Voucher Type", + "options": "DocType", + "read_only": 1 + }, + { + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "label": "Voucher No", + "options": "voucher_type", + "read_only": 1 + }, + { + "fieldname": "against_voucher_type", + "fieldtype": "Link", + "label": "Against Voucher Type", + "options": "DocType", + "read_only": 1 + }, + { + "fieldname": "against_voucher_no", + "fieldtype": "Dynamic Link", + "label": "Against Voucher No", + "options": "against_voucher_type", + "read_only": 1 + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount", + "read_only": 1 + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency", + "read_only": 1 + }, + { + "fieldname": "event", + "fieldtype": "Data", + "label": "Event", + "read_only": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "read_only": 1 + } + ], + "in_create": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2024-11-05 10:31:28.736671", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Advance Payment Ledger Entry", + "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 + } + ], + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.py b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.py new file mode 100644 index 000000000000..0ec2d4117610 --- /dev/null +++ b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.py @@ -0,0 +1,27 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class AdvancePaymentLedgerEntry(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + against_voucher_no: DF.DynamicLink | None + against_voucher_type: DF.Link | None + amount: DF.Currency + company: DF.Link | None + currency: DF.Link | None + event: DF.Data | None + voucher_no: DF.DynamicLink | None + voucher_type: DF.Link | None + # end: auto-generated types + + pass diff --git a/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py b/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py new file mode 100644 index 000000000000..2f578aed1724 --- /dev/null +++ b/erpnext/accounts/doctype/advance_payment_ledger_entry/test_advance_payment_ledger_entry.py @@ -0,0 +1,222 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import nowdate, today + +from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin +from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order + + +class TestAdvancePaymentLedgerEntry(AccountsTestMixin, FrappeTestCase): + """ + Integration tests for AdvancePaymentLedgerEntry. + Use this class for testing interactions between multiple components. + """ + + def setUp(self): + self.create_company() + self.create_usd_receivable_account() + self.create_usd_payable_account() + self.create_item() + self.clear_old_entries() + + def tearDown(self): + frappe.db.rollback() + + def create_sales_order(self, qty=1, rate=100, currency="INR", do_not_submit=False): + """ + Helper method + """ + so = make_sales_order( + company=self.company, + customer=self.customer, + currency=currency, + item=self.item, + qty=qty, + rate=rate, + transaction_date=today(), + do_not_submit=do_not_submit, + ) + return so + + def create_purchase_order(self, qty=1, rate=100, currency="INR", do_not_submit=False): + """ + Helper method + """ + po = create_purchase_order( + company=self.company, + customer=self.supplier, + currency=currency, + item=self.item, + qty=qty, + rate=rate, + transaction_date=today(), + do_not_submit=do_not_submit, + ) + return po + + def test_so_advance_paid_and_currency_with_payment(self): + self.create_customer("_Test USD Customer", "USD") + + so = self.create_sales_order(currency="USD", do_not_submit=True) + so.conversion_rate = 80 + so.submit() + + pe_exchange_rate = 85 + pe = get_payment_entry(so.doctype, so.name, bank_account=self.cash) + pe.reference_no = "1" + pe.reference_date = nowdate() + pe.paid_from = self.debtors_usd + pe.paid_from_account_currency = "USD" + pe.source_exchange_rate = pe_exchange_rate + pe.paid_amount = so.grand_total + pe.received_amount = pe_exchange_rate * pe.paid_amount + pe.references[0].outstanding_amount = 100 + pe.references[0].total_amount = 100 + pe.references[0].allocated_amount = 100 + pe.save().submit() + + so.reload() + self.assertEqual(so.advance_paid, 100) + self.assertEqual(so.party_account_currency, "USD") + + # cancel advance payment + pe.reload() + pe.cancel() + + so.reload() + self.assertEqual(so.advance_paid, 0) + self.assertEqual(so.party_account_currency, "USD") + + def test_so_advance_paid_and_currency_with_journal(self): + self.create_customer("_Test USD Customer", "USD") + + so = self.create_sales_order(currency="USD", do_not_submit=True) + so.conversion_rate = 80 + so.submit() + + je_exchange_rate = 85 + je = frappe.get_doc( + { + "doctype": "Journal Entry", + "company": self.company, + "voucher_type": "Journal Entry", + "posting_date": so.transaction_date, + "multi_currency": True, + "accounts": [ + { + "account": self.debtors_usd, + "party_type": "Customer", + "party": so.customer, + "credit": 8500, + "credit_in_account_currency": 100, + "is_advance": "Yes", + "reference_type": so.doctype, + "reference_name": so.name, + "exchange_rate": je_exchange_rate, + }, + { + "account": self.cash, + "debit": 8500, + "debit_in_account_currency": 8500, + }, + ], + } + ) + je.save().submit() + so.reload() + self.assertEqual(so.advance_paid, 100) + self.assertEqual(so.party_account_currency, "USD") + + # cancel advance payment + je.reload() + je.cancel() + + so.reload() + self.assertEqual(so.advance_paid, 0) + self.assertEqual(so.party_account_currency, "USD") + + def test_po_advance_paid_and_currency_with_payment(self): + self.create_supplier("_Test USD Supplier", "USD") + + po = self.create_purchase_order(currency="USD", do_not_submit=True) + po.conversion_rate = 80 + po.submit() + + pe_exchange_rate = 85 + pe = get_payment_entry(po.doctype, po.name, bank_account=self.cash) + pe.reference_no = "1" + pe.reference_date = nowdate() + pe.paid_to = self.creditors_usd + pe.paid_to_account_currency = "USD" + pe.target_exchange_rate = pe_exchange_rate + pe.received_amount = po.grand_total + pe.paid_amount = pe_exchange_rate * pe.received_amount + pe.references[0].outstanding_amount = 100 + pe.references[0].total_amount = 100 + pe.references[0].allocated_amount = 100 + pe.save().submit() + + po.reload() + self.assertEqual(po.advance_paid, 100) + self.assertEqual(po.party_account_currency, "USD") + + # cancel advance payment + pe.reload() + pe.cancel() + + po.reload() + self.assertEqual(po.advance_paid, 0) + self.assertEqual(po.party_account_currency, "USD") + + def test_po_advance_paid_and_currency_with_journal(self): + self.create_supplier("_Test USD Supplier", "USD") + + po = self.create_purchase_order(currency="USD", do_not_submit=True) + po.conversion_rate = 80 + po.submit() + + je_exchange_rate = 85 + je = frappe.get_doc( + { + "doctype": "Journal Entry", + "company": self.company, + "voucher_type": "Journal Entry", + "posting_date": po.transaction_date, + "multi_currency": True, + "accounts": [ + { + "account": self.creditors_usd, + "party_type": "Supplier", + "party": po.supplier, + "debit": 8500, + "debit_in_account_currency": 100, + "is_advance": "Yes", + "reference_type": po.doctype, + "reference_name": po.name, + "exchange_rate": je_exchange_rate, + }, + { + "account": self.cash, + "credit": 8500, + "credit_in_account_currency": 8500, + }, + ], + } + ) + je.save().submit() + po.reload() + self.assertEqual(po.advance_paid, 100) + self.assertEqual(po.party_account_currency, "USD") + + # cancel advance payment + je.reload() + je.cancel() + + po.reload() + self.assertEqual(po.advance_paid, 0) + self.assertEqual(po.party_account_currency, "USD") diff --git a/erpnext/accounts/doctype/bank_account/bank_account.json b/erpnext/accounts/doctype/bank_account/bank_account.json index 32f1c675d3b1..962551b24179 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.json +++ b/erpnext/accounts/doctype/bank_account/bank_account.json @@ -208,8 +208,49 @@ "label": "Disabled" } ], - "links": [], - "modified": "2023-09-22 21:31:34.763977", + "links": [ + { + "group": "Transactions", + "link_doctype": "Payment Request", + "link_fieldname": "bank_account" + }, + { + "group": "Transactions", + "link_doctype": "Payment Order", + "link_fieldname": "bank_account" + }, + { + "group": "Transactions", + "link_doctype": "Bank Guarantee", + "link_fieldname": "bank_account" + }, + { + "group": "Transactions", + "link_doctype": "Bank Transaction", + "link_fieldname": "bank_account" + }, + { + "group": "Accounting", + "link_doctype": "Payment Entry", + "link_fieldname": "bank_account" + }, + { + "group": "Accounting", + "link_doctype": "Journal Entry", + "link_fieldname": "bank_account" + }, + { + "group": "Party", + "link_doctype": "Customer", + "link_fieldname": "default_bank_account" + }, + { + "group": "Party", + "link_doctype": "Supplier", + "link_fieldname": "default_bank_account" + } + ], + "modified": "2024-10-30 09:41:14.113414", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Account", @@ -246,4 +287,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/bank_account/bank_account_dashboard.py b/erpnext/accounts/doctype/bank_account/bank_account_dashboard.py deleted file mode 100644 index 8bf8d8a5cd01..000000000000 --- a/erpnext/accounts/doctype/bank_account/bank_account_dashboard.py +++ /dev/null @@ -1,20 +0,0 @@ -from frappe import _ - - -def get_data(): - return { - "fieldname": "bank_account", - "non_standard_fieldnames": { - "Customer": "default_bank_account", - "Supplier": "default_bank_account", - }, - "transactions": [ - { - "label": _("Payments"), - "items": ["Payment Entry", "Payment Request", "Payment Order", "Payroll Entry"], - }, - {"label": _("Party"), "items": ["Customer", "Supplier"]}, - {"items": ["Bank Guarantee"]}, - {"items": ["Journal Entry"]}, - ], - } diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.js b/erpnext/accounts/doctype/bank_clearance/bank_clearance.js index 2993825482c7..7ece7c9c2d6d 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.js +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.js @@ -38,6 +38,11 @@ frappe.ui.form.on("Bank Clearance", { frm.add_custom_button(__("Get Payment Entries"), () => frm.trigger("get_payment_entries")); frm.change_custom_button_type(__("Get Payment Entries"), null, "primary"); + if (frm.doc.payment_entries.length) { + frm.add_custom_button(__("Update Clearance Date"), () => frm.trigger("update_clearance_date")); + frm.change_custom_button_type(__("Get Payment Entries"), null, "default"); + frm.change_custom_button_type(__("Update Clearance Date"), null, "primary"); + } }, update_clearance_date: function (frm) { @@ -45,13 +50,7 @@ frappe.ui.form.on("Bank Clearance", { method: "update_clearance_date", doc: frm.doc, callback: function (r, rt) { - frm.refresh_field("payment_entries"); - frm.refresh_fields(); - - if (!frm.doc.payment_entries.length) { - frm.change_custom_button_type(__("Get Payment Entries"), null, "primary"); - frm.change_custom_button_type(__("Update Clearance Date"), null, "default"); - } + frm.refresh(); }, }); }, @@ -60,17 +59,8 @@ frappe.ui.form.on("Bank Clearance", { return frappe.call({ method: "get_payment_entries", doc: frm.doc, - callback: function (r, rt) { - frm.refresh_field("payment_entries"); - - if (frm.doc.payment_entries.length) { - frm.add_custom_button(__("Update Clearance Date"), () => - frm.trigger("update_clearance_date") - ); - - frm.change_custom_button_type(__("Get Payment Entries"), null, "default"); - frm.change_custom_button_type(__("Update Clearance Date"), null, "primary"); - } + callback: function () { + frm.refresh(); }, }); }, diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index 63758a5e7fb6..ac7883fce138 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -6,7 +6,7 @@ from frappe import _, msgprint from frappe.model.document import Document from frappe.query_builder.custom import ConstantColumn -from frappe.utils import flt, fmt_money, getdate +from frappe.utils import flt, fmt_money, get_link_to_form, getdate from pypika import Order import erpnext @@ -96,8 +96,11 @@ def update_clearance_date(self): if d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date): frappe.throw( - _("Row #{0}: Clearance date {1} cannot be before Cheque Date {2}").format( - d.idx, d.clearance_date, d.cheque_date + _("Row #{0}: For {1} Clearance date {2} cannot be before Cheque Date {3}").format( + d.idx, + get_link_to_form(d.payment_document, d.payment_entry), + d.clearance_date, + d.cheque_date, ) ) @@ -105,8 +108,18 @@ def update_clearance_date(self): if not d.clearance_date: d.clearance_date = None - payment_entry = frappe.get_doc(d.payment_document, d.payment_entry) - payment_entry.db_set("clearance_date", d.clearance_date) + if d.payment_document == "Sales Invoice": + frappe.db.set_value( + "Sales Invoice Payment", + {"parent": d.payment_entry, "account": self.get("account"), "amount": [">", 0]}, + "clearance_date", + d.clearance_date, + ) + + else: + frappe.db.set_value( + d.payment_document, d.payment_entry, "clearance_date", d.clearance_date + ) clearance_date_updated = True @@ -155,7 +168,7 @@ def get_payment_entries_for_bank_clearance( "Payment Entry" as payment_document, name as payment_entry, reference_no as cheque_number, reference_date as cheque_date, if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit, - if(paid_from=%(account)s, 0, received_amount) as debit, + if(paid_from=%(account)s, 0, received_amount + total_taxes_and_charges) as debit, posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date, if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency from `tabPayment Entry` diff --git a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py index d785bfbfef22..658a69a48037 100644 --- a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py @@ -6,16 +6,29 @@ import frappe from frappe.utils import add_months, getdate +from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.stock.doctype.item.test_item import create_item +from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.tests.utils import if_lending_app_installed, if_lending_app_not_installed class TestBankClearance(unittest.TestCase): @classmethod def setUpClass(cls): + create_warehouse( + warehouse_name="_Test Warehouse", + properties={"parent_warehouse": "All Warehouses - _TC"}, + company="_Test Company", + ) + create_item("_Test Item") + create_cost_center(cost_center_name="_Test Cost Center", company="_Test Company") + clear_payment_entries() clear_loan_transactions() + clear_pos_sales_invoices() make_bank_account() add_transactions() @@ -83,11 +96,41 @@ def make_loan(): bank_clearance.get_payment_entries() self.assertEqual(len(bank_clearance.payment_entries), 3) + def test_update_clearance_date_on_si(self): + sales_invoice = make_pos_sales_invoice() + + date = getdate() + bank_clearance = frappe.get_doc("Bank Clearance") + bank_clearance.account = "_Test Bank Clearance - _TC" + bank_clearance.from_date = add_months(date, -1) + bank_clearance.to_date = date + bank_clearance.include_pos_transactions = 1 + bank_clearance.get_payment_entries() + + self.assertNotEqual(len(bank_clearance.payment_entries), 0) + for payment in bank_clearance.payment_entries: + if payment.payment_entry == sales_invoice.name: + payment.clearance_date = date + + bank_clearance.update_clearance_date() + + si_clearance_date = frappe.db.get_value( + "Sales Invoice Payment", + {"parent": sales_invoice.name, "account": bank_clearance.account}, + "clearance_date", + ) + + self.assertEqual(si_clearance_date, date) + def clear_payment_entries(): frappe.db.delete("Payment Entry") +def clear_pos_sales_invoices(): + frappe.db.delete("Sales Invoice", {"is_pos": 1}) + + @if_lending_app_installed def clear_loan_transactions(): for dt in [ @@ -115,9 +158,45 @@ def add_transactions(): def make_payment_entry(): - pi = make_purchase_invoice(supplier="_Test Supplier", qty=1, rate=690) + from erpnext.buying.doctype.supplier.test_supplier import create_supplier + + supplier = create_supplier(supplier_name="_Test Supplier") + pi = make_purchase_invoice( + supplier=supplier, + supplier_warehouse="_Test Warehouse - _TC", + expense_account="Cost of Goods Sold - _TC", + uom="Nos", + qty=1, + rate=690, + ) pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank Clearance - _TC") pe.reference_no = "Conrad Oct 18" pe.reference_date = "2018-10-24" pe.insert() pe.submit() + + +def make_pos_sales_invoice(): + from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import ( + make_customer, + ) + + mode_of_payment = frappe.get_doc({"doctype": "Mode of Payment", "name": "Cash"}) + + if not frappe.db.get_value("Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}): + mode_of_payment.append( + "accounts", {"company": "_Test Company", "default_account": "_Test Bank Clearance - _TC"} + ) + mode_of_payment.save() + + customer = make_customer(customer="_Test Customer") + + si = create_sales_invoice(customer=customer, item="_Test Item", is_pos=1, qty=1, rate=1000, do_not_save=1) + si.set("payments", []) + si.append( + "payments", {"mode_of_payment": "Cash", "account": "_Test Bank Clearance - _TC", "amount": 1000} + ) + si.insert() + si.submit() + + return si diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index 4354f238a422..c13dbe445f18 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -208,13 +208,17 @@ def auto_set_party(self): if self.party_type and self.party: return - result = AutoMatchParty( - bank_party_account_number=self.bank_party_account_number, - bank_party_iban=self.bank_party_iban, - bank_party_name=self.bank_party_name, - description=self.description, - deposit=self.deposit, - ).match() + result = None + try: + result = AutoMatchParty( + bank_party_account_number=self.bank_party_account_number, + bank_party_iban=self.bank_party_iban, + bank_party_name=self.bank_party_name, + description=self.description, + deposit=self.deposit, + ).match() + except Exception: + frappe.log_error(title=_("Error in party matching for Bank Transaction {0}").format(self.name)) if not result: return diff --git a/erpnext/accounts/doctype/cost_center_allocation/test_cost_center_allocation.py b/erpnext/accounts/doctype/cost_center_allocation/test_cost_center_allocation.py index 65784dbb6c72..4abc82d8becd 100644 --- a/erpnext/accounts/doctype/cost_center_allocation/test_cost_center_allocation.py +++ b/erpnext/accounts/doctype/cost_center_allocation/test_cost_center_allocation.py @@ -22,8 +22,10 @@ def setUp(self): cost_centers = [ "Main Cost Center 1", "Main Cost Center 2", + "Main Cost Center 3", "Sub Cost Center 1", "Sub Cost Center 2", + "Sub Cost Center 3", ] for cc in cost_centers: create_cost_center(cost_center_name=cc, company="_Test Company") @@ -36,7 +38,7 @@ def test_gle_based_on_cost_center_allocation(self): ) jv = make_journal_entry( - "_Test Cash - _TC", "Sales - _TC", 100, cost_center="Main Cost Center 1 - _TC", submit=True + "Cash - _TC", "Sales - _TC", 100, cost_center="Main Cost Center 1 - _TC", submit=True ) expected_values = [["Sub Cost Center 1 - _TC", 0.0, 60], ["Sub Cost Center 2 - _TC", 0.0, 40]] @@ -120,7 +122,7 @@ def test_total_percentage(self): def test_valid_from_based_on_existing_gle(self): # GLE posted against Sub Cost Center 1 on today jv = make_journal_entry( - "_Test Cash - _TC", + "Cash - _TC", "Sales - _TC", 100, cost_center="Main Cost Center 1 - _TC", @@ -141,6 +143,53 @@ def test_valid_from_based_on_existing_gle(self): jv.cancel() + def test_multiple_cost_center_allocation_on_same_main_cost_center(self): + coa1 = create_cost_center_allocation( + "_Test Company", + "Main Cost Center 3 - _TC", + {"Sub Cost Center 1 - _TC": 30, "Sub Cost Center 2 - _TC": 30, "Sub Cost Center 3 - _TC": 40}, + valid_from=add_days(today(), -5), + ) + + coa2 = create_cost_center_allocation( + "_Test Company", + "Main Cost Center 3 - _TC", + {"Sub Cost Center 1 - _TC": 50, "Sub Cost Center 2 - _TC": 50}, + valid_from=add_days(today(), -1), + ) + + jv = make_journal_entry( + "Cash - _TC", + "Sales - _TC", + 100, + cost_center="Main Cost Center 3 - _TC", + posting_date=today(), + submit=True, + ) + + expected_values = {"Sub Cost Center 1 - _TC": 50, "Sub Cost Center 2 - _TC": 50} + + gle = frappe.qb.DocType("GL Entry") + gl_entries = ( + frappe.qb.from_(gle) + .select(gle.cost_center, gle.debit, gle.credit) + .where(gle.voucher_type == "Journal Entry") + .where(gle.voucher_no == jv.name) + .where(gle.account == "Sales - _TC") + .orderby(gle.cost_center) + ).run(as_dict=1) + + self.assertTrue(gl_entries) + + for gle in gl_entries: + self.assertTrue(gle.cost_center in expected_values) + self.assertEqual(gle.debit, 0) + self.assertEqual(gle.credit, expected_values[gle.cost_center]) + + coa1.cancel() + coa2.cancel() + jv.cancel() + def create_cost_center_allocation( company, diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py index 8cbb99e9252d..160e791978e8 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py @@ -109,7 +109,7 @@ def get_api_endpoint(service_provider: str | None = None, use_http: bool = False if service_provider == "exchangerate.host": api = "api.exchangerate.host/convert" elif service_provider == "frankfurter.app": - api = "frankfurter.app/{transaction_date}" + api = "api.frankfurter.app/{transaction_date}" protocol = "https://" if use_http: diff --git a/erpnext/accounts/doctype/dunning/dunning.json b/erpnext/accounts/doctype/dunning/dunning.json index b7e8aeaaafd6..496097417bad 100644 --- a/erpnext/accounts/doctype/dunning/dunning.json +++ b/erpnext/accounts/doctype/dunning/dunning.json @@ -19,16 +19,6 @@ "currency", "column_break_11", "conversion_rate", - "address_and_contact_section", - "customer_address", - "address_display", - "contact_person", - "contact_display", - "column_break_16", - "company_address", - "company_address_display", - "contact_mobile", - "contact_email", "section_break_6", "dunning_type", "column_break_8", @@ -56,7 +46,21 @@ "income_account", "column_break_48", "cost_center", - "amended_from" + "amended_from", + "address_and_contact_tab", + "address_and_contact_section", + "customer_address", + "address_display", + "column_break_vodj", + "contact_person", + "contact_display", + "contact_mobile", + "contact_email", + "section_break_xban", + "column_break_16", + "company_address", + "company_address_display", + "column_break_lqmf" ], "fields": [ { @@ -178,10 +182,8 @@ "label": "Rate of Interest (%) Yearly" }, { - "collapsible": 1, "fieldname": "address_and_contact_section", - "fieldtype": "Section Break", - "label": "Address and Contact" + "fieldtype": "Section Break" }, { "fieldname": "address_display", @@ -377,11 +379,28 @@ { "fieldname": "column_break_48", "fieldtype": "Column Break" + }, + { + "fieldname": "address_and_contact_tab", + "fieldtype": "Tab Break", + "label": "Address & Contact" + }, + { + "fieldname": "column_break_vodj", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_xban", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_lqmf", + "fieldtype": "Column Break" } ], "is_submittable": 1, "links": [], - "modified": "2023-06-15 15:46:53.865712", + "modified": "2024-11-26 13:46:07.760867", "modified_by": "Administrator", "module": "Accounts", "name": "Dunning", @@ -435,4 +454,4 @@ "states": [], "title_field": "customer_name", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index ad58761e1bfd..d63db3a09a11 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -210,19 +210,31 @@ def get_linked_dunnings_as_per_state(sales_invoice, state): @frappe.whitelist() -def get_dunning_letter_text(dunning_type, doc, language=None): +def get_dunning_letter_text(dunning_type: str, doc: str | dict, language: str | None = None) -> dict: + DOCTYPE = "Dunning Letter Text" + FIELDS = ["body_text", "closing_text", "language"] + if isinstance(doc, str): doc = json.loads(doc) + + if not language: + language = doc.get("language") + if language: - filters = {"parent": dunning_type, "language": language} - else: - filters = {"parent": dunning_type, "is_default_language": 1} - letter_text = frappe.db.get_value( - "Dunning Letter Text", filters, ["body_text", "closing_text", "language"], as_dict=1 - ) - if letter_text: - return { - "body_text": frappe.render_template(letter_text.body_text, doc), - "closing_text": frappe.render_template(letter_text.closing_text, doc), - "language": letter_text.language, - } + letter_text = frappe.db.get_value( + DOCTYPE, {"parent": dunning_type, "language": language}, FIELDS, as_dict=1 + ) + + if not letter_text: + letter_text = frappe.db.get_value( + DOCTYPE, {"parent": dunning_type, "is_default_language": 1}, FIELDS, as_dict=1 + ) + + if not letter_text: + return {} + + return { + "body_text": frappe.render_template(letter_text.body_text, doc), + "closing_text": frappe.render_template(letter_text.closing_text, doc), + "language": letter_text.language, + } diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index 8607d1ed71f7..c08bd3878d58 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -74,6 +74,21 @@ def validate_mandatory(self): if not (self.company and self.posting_date): frappe.throw(_("Please select Company and Posting Date to getting entries")) + def before_submit(self): + self.remove_accounts_without_gain_loss() + + def remove_accounts_without_gain_loss(self): + self.accounts = [account for account in self.accounts if account.gain_loss] + + if not self.accounts: + frappe.throw(_("At least one account with exchange gain or loss is required")) + + frappe.msgprint( + _("Removing rows without exchange gain or loss"), + alert=True, + indicator="yellow", + ) + def on_cancel(self): self.ignore_linked_doctypes = "GL Entry" @@ -248,23 +263,23 @@ def calculate_new_account_balance(company, posting_date, account_details): new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, posting_date) new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate) gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision) - if gain_loss: - accounts.append( - { - "account": d.account, - "party_type": d.party_type, - "party": d.party, - "account_currency": d.account_currency, - "balance_in_base_currency": d.balance, - "balance_in_account_currency": d.balance_in_account_currency, - "zero_balance": d.zero_balance, - "current_exchange_rate": current_exchange_rate, - "new_exchange_rate": new_exchange_rate, - "new_balance_in_base_currency": new_balance_in_base_currency, - "new_balance_in_account_currency": d.balance_in_account_currency, - "gain_loss": gain_loss, - } - ) + + accounts.append( + { + "account": d.account, + "party_type": d.party_type, + "party": d.party, + "account_currency": d.account_currency, + "balance_in_base_currency": d.balance, + "balance_in_account_currency": d.balance_in_account_currency, + "zero_balance": d.zero_balance, + "current_exchange_rate": current_exchange_rate, + "new_exchange_rate": new_exchange_rate, + "new_balance_in_base_currency": new_balance_in_base_currency, + "new_balance_in_account_currency": d.balance_in_account_currency, + "gain_loss": gain_loss, + } + ) # Handle Accounts with '0' balance in Account/Base Currency for d in [x for x in account_details if x.zero_balance]: @@ -288,23 +303,22 @@ def calculate_new_account_balance(company, posting_date, account_details): current_exchange_rate * d.balance_in_account_currency ) - if gain_loss: - accounts.append( - { - "account": d.account, - "party_type": d.party_type, - "party": d.party, - "account_currency": d.account_currency, - "balance_in_base_currency": d.balance, - "balance_in_account_currency": d.balance_in_account_currency, - "zero_balance": d.zero_balance, - "current_exchange_rate": current_exchange_rate, - "new_exchange_rate": new_exchange_rate, - "new_balance_in_base_currency": new_balance_in_base_currency, - "new_balance_in_account_currency": new_balance_in_account_currency, - "gain_loss": gain_loss, - } - ) + accounts.append( + { + "account": d.account, + "party_type": d.party_type, + "party": d.party, + "account_currency": d.account_currency, + "balance_in_base_currency": d.balance, + "balance_in_account_currency": d.balance_in_account_currency, + "zero_balance": d.zero_balance, + "current_exchange_rate": current_exchange_rate, + "new_exchange_rate": new_exchange_rate, + "new_balance_in_base_currency": new_balance_in_base_currency, + "new_balance_in_account_currency": new_balance_in_account_currency, + "gain_loss": gain_loss, + } + ) return accounts diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py index 51053f1f68c7..3eef6ab3832d 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py @@ -188,7 +188,7 @@ def test_03_accounts_only_with_account_currency_balance(self): pe = get_payment_entry(si.doctype, si.name) pe.paid_amount = 95 - pe.source_exchange_rate = 84.211 + pe.source_exchange_rate = 84.2105 pe.received_amount = 8000 pe.references = [] pe.save().submit() @@ -229,7 +229,7 @@ def test_03_accounts_only_with_account_currency_balance(self): row = next(x for x in je.accounts if x.account == self.debtors_usd) self.assertEqual(flt(row.credit_in_account_currency, precision), 5.0) # in USD row = next(x for x in je.accounts if x.account != self.debtors_usd) - self.assertEqual(flt(row.debit_in_account_currency, precision), 421.06) # in INR + self.assertEqual(flt(row.debit_in_account_currency, precision), 421.05) # in INR # total_debit and total_credit will be 0.0, as JV is posting only to account currency fields self.assertEqual(flt(je.total_debit, precision), 0.0) diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.js b/erpnext/accounts/doctype/fiscal_year/fiscal_year.js index a44b52f08f8f..aeb9f982b4d3 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.js +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.js @@ -4,10 +4,7 @@ frappe.ui.form.on("Fiscal Year", { onload: function (frm) { if (frm.doc.__islocal) { - frm.set_value( - "year_start_date", - frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1) - ); + frm.set_value("year_start_date", frappe.datetime.year_start()); } }, year_start_date: function (frm) { diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.json b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json index 66db37fe13b0..de8f0337a3dd 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.json +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json @@ -72,10 +72,10 @@ }, { "default": "0", - "description": "Less than 12 months.", + "description": "More/Less than 12 months.", "fieldname": "is_short_year", "fieldtype": "Check", - "label": "Is Short Year", + "label": "Is Short/Long Year", "set_only_once": 1 } ], diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.json b/erpnext/accounts/doctype/gl_entry/gl_entry.json index 2d106ad8cee8..c285a33f73e6 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.json +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.json @@ -6,38 +6,50 @@ "document_type": "Document", "engine": "InnoDB", "field_order": [ + "dates_section", "posting_date", "transaction_date", + "column_break_avko", + "fiscal_year", + "due_date", + "account_details_section", "account", - "party_type", - "party", - "cost_center", - "debit", - "credit", "account_currency", - "debit_in_account_currency", - "credit_in_account_currency", + "column_break_ifvf", "against", - "against_voucher_type", - "against_voucher", + "party_type", + "party", + "transaction_details_section", "voucher_type", - "voucher_subtype", "voucher_no", + "voucher_subtype", + "transaction_currency", + "column_break_dpsx", + "against_voucher_type", + "against_voucher", "voucher_detail_no", + "transaction_exchange_rate", + "amounts_section", + "debit_in_account_currency", + "debit", + "debit_in_transaction_currency", + "column_break_bm1w", + "credit_in_account_currency", + "credit", + "credit_in_transaction_currency", + "dimensions_section", + "cost_center", + "column_break_lmnm", "project", - "remarks", + "more_info_section", + "finance_book", + "company", "is_opening", "is_advance", - "fiscal_year", - "company", - "finance_book", + "column_break_8abq", "to_rename", - "due_date", "is_cancelled", - "transaction_currency", - "debit_in_transaction_currency", - "credit_in_transaction_currency", - "transaction_exchange_rate" + "remarks" ], "fields": [ { @@ -285,13 +297,67 @@ "fieldname": "voucher_subtype", "fieldtype": "Small Text", "label": "Voucher Subtype" + }, + { + "fieldname": "dates_section", + "fieldtype": "Section Break", + "label": "Dates" + }, + { + "fieldname": "column_break_avko", + "fieldtype": "Column Break" + }, + { + "fieldname": "account_details_section", + "fieldtype": "Section Break", + "label": "Account Details" + }, + { + "fieldname": "column_break_ifvf", + "fieldtype": "Column Break" + }, + { + "fieldname": "transaction_details_section", + "fieldtype": "Section Break", + "label": "Transaction Details" + }, + { + "fieldname": "amounts_section", + "fieldtype": "Section Break", + "label": "Amounts" + }, + { + "fieldname": "column_break_dpsx", + "fieldtype": "Column Break" + }, + { + "fieldname": "more_info_section", + "fieldtype": "Section Break", + "label": "More Info" + }, + { + "fieldname": "column_break_bm1w", + "fieldtype": "Column Break" + }, + { + "fieldname": "dimensions_section", + "fieldtype": "Section Break", + "label": "Dimensions" + }, + { + "fieldname": "column_break_lmnm", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_8abq", + "fieldtype": "Column Break" } ], "icon": "fa fa-list", "idx": 1, "in_create": 1, "links": [], - "modified": "2024-07-02 14:31:51.496466", + "modified": "2024-08-22 13:03:39.997475", "modified_by": "Administrator", "module": "Accounts", "name": "GL Entry", diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index d74224c4aa2c..a7e7edb098dd 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -430,8 +430,9 @@ def update_against_account(voucher_type, voucher_no): def on_doctype_update(): - frappe.db.add_index("GL Entry", ["against_voucher_type", "against_voucher"]) frappe.db.add_index("GL Entry", ["voucher_type", "voucher_no"]) + frappe.db.add_index("GL Entry", ["posting_date", "company"]) + frappe.db.add_index("GL Entry", ["party_type", "party"]) def rename_gle_sle_docs(): diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index d290d794df19..faa38763b80d 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -360,21 +360,23 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro accounts_add(doc, cdt, cdn) { var row = frappe.get_doc(cdt, cdn); + row.exchange_rate = 1; $.each(doc.accounts, function (i, d) { if (d.account && d.party && d.party_type) { row.account = d.account; row.party = d.party; row.party_type = d.party_type; + row.exchange_rate = d.exchange_rate; } }); // set difference if (doc.difference) { if (doc.difference > 0) { - row.credit_in_account_currency = doc.difference; + row.credit_in_account_currency = doc.difference / row.exchange_rate; row.credit = doc.difference; } else { - row.debit_in_account_currency = -doc.difference; + row.debit_in_account_currency = -doc.difference / row.exchange_rate; row.debit = -doc.difference; } } @@ -680,6 +682,7 @@ $.extend(erpnext.journal_entry, { callback: function (r) { if (r.message) { $.extend(d, r.message); + erpnext.journal_entry.set_amount_on_last_row(frm, dt, dn); erpnext.journal_entry.set_debit_credit_in_company_currency(frm, dt, dn); refresh_field("accounts"); } @@ -687,4 +690,26 @@ $.extend(erpnext.journal_entry, { }); } }, + set_amount_on_last_row: function (frm, dt, dn) { + let row = locals[dt][dn]; + let length = frm.doc.accounts.length; + if (row.idx != length) return; + + let difference = frm.doc.accounts.reduce((total, row) => { + if (row.idx == length) return total; + + return total + row.debit - row.credit; + }, 0); + + if (difference) { + if (difference > 0) { + row.credit_in_account_currency = difference / row.exchange_rate; + row.credit = difference; + } else { + row.debit_in_account_currency = -difference / row.exchange_rate; + row.debit = -difference; + } + } + refresh_field("accounts"); + }, }); diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 6adc8be3f7db..ef2388a7eaab 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -127,9 +127,6 @@ def validate(self): self.set_amounts_in_company_currency() self.validate_debit_credit_amount() self.set_total_debit_credit() - # Do not validate while importing via data import - if not frappe.flags.in_import: - self.validate_total_debit_and_credit() if not frappe.flags.is_reverse_depr_entry: self.validate_against_jv() @@ -184,10 +181,16 @@ def cancel(self): else: return self._cancel() + def before_submit(self): + # Do not validate while importing via data import + if not frappe.flags.in_import: + self.validate_total_debit_and_credit() + def on_submit(self): self.validate_cheque_info() self.check_credit_limit() self.make_gl_entries() + self.make_advance_payment_ledger_entries() self.update_advance_paid() self.update_asset_value() self.update_inter_company_jv() @@ -195,6 +198,11 @@ def on_submit(self): self.update_booked_depreciation() def on_update_after_submit(self): + # Flag will be set on Reconciliation + # Reconciliation tool will anyways repost ledger entries. So, no need to check and do implicit repost. + if self.flags.get("ignore_reposting_on_reconciliation"): + return + self.needs_repost = self.check_if_fields_updated(fields_to_check=[], child_tables={"accounts": []}) if self.needs_repost: self.validate_for_repost() @@ -213,8 +221,10 @@ def on_cancel(self): "Repost Accounting Ledger Items", "Unreconcile Payment", "Unreconcile Payment Entries", + "Advance Payment Ledger Entry", ) self.make_gl_entries(1) + self.make_advance_payment_ledger_entries() self.update_advance_paid() self.unlink_advance_entry_reference() self.unlink_asset_reference() @@ -254,7 +264,7 @@ def validate_depr_entry_voucher_type(self): frappe.throw(_("Journal Entry type should be set as Depreciation Entry for asset depreciation")) def validate_stock_accounts(self): - stock_accounts = get_stock_accounts(self.company, self.doctype, self.name) + stock_accounts = get_stock_accounts(self.company, accounts=self.accounts) for account in stock_accounts: account_bal, stock_bal, warehouse_list = get_stock_and_account_balance( account, self.posting_date, self.company @@ -1663,6 +1673,8 @@ def post_process(source, target): "debit": "credit", "credit_in_account_currency": "debit_in_account_currency", "credit": "debit", + "reference_type": "reference_type", + "reference_name": "reference_name", }, }, }, diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py index c53faf9ff390..8f4c4e3ccda3 100644 --- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py @@ -515,6 +515,72 @@ def test_negative_debit_and_credit_with_same_account_head(self): self.assertEqual(row.debit_in_account_currency, 100) self.assertEqual(row.credit_in_account_currency, 100) + def test_toggle_debit_credit_if_negative(self): + from erpnext.accounts.general_ledger import process_gl_map + + # Create JV with defaut cost center - _Test Cost Center + frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0) + + jv = frappe.new_doc("Journal Entry") + jv.posting_date = nowdate() + jv.company = "_Test Company" + jv.user_remark = "test" + jv.extend( + "accounts", + [ + { + "account": "_Test Cash - _TC", + "debit": 100 * -1, + "debit_in_account_currency": 100 * -1, + "exchange_rate": 1, + }, + { + "account": "_Test Bank - _TC", + "credit": 100 * -1, + "credit_in_account_currency": 100 * -1, + "exchange_rate": 1, + }, + ], + ) + + jv.flags.ignore_validate = True + jv.save() + + self.assertEqual(len(jv.accounts), 2) + + gl_map = jv.build_gl_map() + + for row in gl_map: + if row.account == "_Test Cash - _TC": + self.assertEqual(row.debit, 100 * -1) + self.assertEqual(row.debit_in_account_currency, 100 * -1) + self.assertEqual(row.debit_in_transaction_currency, 100 * -1) + + gl_map = process_gl_map(gl_map, False) + + for row in gl_map: + if row.account == "_Test Cash - _TC": + self.assertEqual(row.credit, 100) + self.assertEqual(row.credit_in_account_currency, 100) + self.assertEqual(row.credit_in_transaction_currency, 100) + + def test_transaction_exchange_rate_on_journals(self): + jv = make_journal_entry("_Test Bank - _TC", "_Test Receivable USD - _TC", 100, save=False) + jv.accounts[0].update({"debit_in_account_currency": 8500, "exchange_rate": 1}) + jv.accounts[1].update({"party_type": "Customer", "party": "_Test Customer USD", "exchange_rate": 85}) + jv.submit() + actual = frappe.db.get_all( + "GL Entry", + filters={"voucher_no": jv.name, "is_cancelled": 0}, + fields=["account", "transaction_exchange_rate"], + order_by="account", + ) + expected = [ + {"account": "_Test Bank - _TC", "transaction_exchange_rate": 1.0}, + {"account": "_Test Receivable USD - _TC", "transaction_exchange_rate": 85.0}, + ] + self.assertEqual(expected, actual) + def make_journal_entry( account1, diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js index f1efba8a954f..4938e6690e53 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js @@ -28,7 +28,12 @@ frappe.ui.form.on("Opening Invoice Creation Tool", { frm.refresh_fields(); frm.page.clear_indicator(); frm.dashboard.hide_progress(); - frappe.msgprint(__("Opening {0} Invoices created", [frm.doc.invoice_type])); + + if (frm.doc.invoice_type == "Sales") { + frappe.msgprint(__("Opening Sales Invoices have been created.")); + } else { + frappe.msgprint(__("Opening Purchase Invoices have been created.")); + } }, 1500, data.title @@ -48,12 +53,19 @@ frappe.ui.form.on("Opening Invoice Creation Tool", { !frm.doc.import_in_progress && frm.trigger("make_dashboard"); frm.page.set_primary_action(__("Create Invoices"), () => { let btn_primary = frm.page.btn_primary.get(0); + let freeze_message; + if (frm.doc.invoice_type == "Sales") { + freeze_message = __("Creating Sales Invoices ..."); + } else { + freeze_message = __("Creating Purchase Invoices ..."); + } + return frm.call({ doc: frm.doc, btn: $(btn_primary), method: "make_invoices", freeze: 1, - freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type]), + freeze_message: freeze_message, }); }); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index c28dcf525df1..f2d11ba9ff3c 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -26,6 +26,10 @@ frappe.ui.form.on("Payment Entry", { } erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); + + if (frm.is_new()) { + set_default_party_type(frm); + } }, setup: function (frm) { @@ -35,6 +39,11 @@ frappe.ui.form.on("Payment Entry", { var account_types = ["Pay", "Internal Transfer"].includes(frm.doc.payment_type) ? ["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]]; + + if (frm.doc.party_type == "Shareholder") { + account_types.push("Equity"); + } + return { filters: { account_type: ["in", account_types], @@ -90,6 +99,9 @@ frappe.ui.form.on("Payment Entry", { var account_types = ["Receive", "Internal Transfer"].includes(frm.doc.payment_type) ? ["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]]; + if (frm.doc.party_type == "Shareholder") { + account_types.push("Equity"); + } return { filters: { account_type: ["in", account_types], @@ -166,6 +178,21 @@ frappe.ui.form.on("Payment Entry", { }; }); + frm.set_query("payment_request", "references", function (doc, cdt, cdn) { + const row = frappe.get_doc(cdt, cdn); + return { + query: "erpnext.accounts.doctype.payment_request.payment_request.get_open_payment_requests_query", + filters: { + reference_doctype: row.reference_doctype, + reference_name: row.reference_name, + company: doc.company, + status: ["!=", "Paid"], + outstanding_amount: [">", 0], // for compatibility with old data + docstatus: 1, + }, + }; + }); + frm.set_query("sales_taxes_and_charges_template", function () { return { filters: { @@ -183,7 +210,15 @@ frappe.ui.form.on("Payment Entry", { }, }; }); + + frm.add_fetch( + "payment_request", + "outstanding_amount", + "payment_request_outstanding", + "Payment Entry Reference" + ); }, + refresh: function (frm) { erpnext.hide_company(frm); frm.events.hide_unhide_fields(frm); @@ -208,6 +243,7 @@ frappe.ui.form.on("Payment Entry", { ); } erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm); + frappe.flags.allocate_payment_amount = true; }, validate_company: (frm) => { @@ -288,16 +324,11 @@ frappe.ui.form.on("Payment Entry", { "write_off_difference_amount", frm.doc.difference_amount && frm.doc.party && frm.doc.total_allocated_amount > party_amount ); - - frm.toggle_display( - "set_exchange_gain_loss", - frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount - ); }, set_dynamic_labels: function (frm) { var company_currency = frm.doc.company - ? frappe.get_doc(":Company", frm.doc.company).default_currency + ? frappe.get_doc(":Company", frm.doc.company)?.default_currency : ""; frm.set_currency_labels( @@ -375,9 +406,19 @@ frappe.ui.form.on("Payment Entry", { }, payment_type: function (frm) { + set_default_party_type(frm); + if (frm.doc.payment_type == "Internal Transfer") { $.each( - ["party", "party_balance", "paid_from", "paid_to", "references", "total_allocated_amount"], + [ + "party", + "party_type", + "party_balance", + "paid_from", + "paid_to", + "references", + "total_allocated_amount", + ], function (i, field) { frm.set_value(field, null); } @@ -412,6 +453,12 @@ frappe.ui.form.on("Payment Entry", { return { query: "erpnext.controllers.queries.employee_query", }; + } else if (frm.doc.party_type == "Shareholder") { + return { + filters: { + company: frm.doc.company, + }, + }; } }); @@ -644,7 +691,7 @@ frappe.ui.form.on("Payment Entry", { frm.set_value("source_exchange_rate", 1); } else if (frm.doc.paid_from) { if (["Internal Transfer", "Pay"].includes(frm.doc.payment_type)) { - let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; + let company_currency = frappe.get_doc(":Company", frm.doc.company)?.default_currency; frappe.call({ method: "erpnext.setup.utils.get_exchange_rate", args: { @@ -775,7 +822,7 @@ frappe.ui.form.on("Payment Entry", { ); if (frm.doc.payment_type == "Pay") - frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, 1); + frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, true); else frm.events.set_unallocated_amount(frm); frm.set_paid_amount_based_on_received_amount = false; @@ -796,7 +843,7 @@ frappe.ui.form.on("Payment Entry", { } if (frm.doc.payment_type == "Receive") - frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, 1); + frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, true); else frm.events.set_unallocated_amount(frm); }, @@ -967,6 +1014,7 @@ frappe.ui.form.on("Payment Entry", { c.outstanding_amount = d.outstanding_amount; c.bill_no = d.bill_no; c.payment_term = d.payment_term; + c.payment_term_outstanding = d.payment_term_outstanding; c.allocated_amount = d.allocated_amount; c.account = d.account; @@ -1016,7 +1064,8 @@ frappe.ui.form.on("Payment Entry", { frm.events.allocate_party_amount_against_ref_docs( frm, - frm.doc.payment_type == "Receive" ? frm.doc.paid_amount : frm.doc.received_amount + frm.doc.payment_type == "Receive" ? frm.doc.paid_amount : frm.doc.received_amount, + false ); }, }); @@ -1030,93 +1079,13 @@ frappe.ui.form.on("Payment Entry", { return ["Sales Invoice", "Purchase Invoice"]; }, - allocate_party_amount_against_ref_docs: function (frm, paid_amount, paid_amount_change) { - var total_positive_outstanding_including_order = 0; - var total_negative_outstanding = 0; - var total_deductions = frappe.utils.sum( - $.map(frm.doc.deductions || [], function (d) { - return flt(d.amount); - }) - ); - - paid_amount -= total_deductions; - - $.each(frm.doc.references || [], function (i, row) { - if (flt(row.outstanding_amount) > 0) - total_positive_outstanding_including_order += flt(row.outstanding_amount); - else total_negative_outstanding += Math.abs(flt(row.outstanding_amount)); - }); - - var allocated_negative_outstanding = 0; - if ( - (frm.doc.payment_type == "Receive" && frm.doc.party_type == "Customer") || - (frm.doc.payment_type == "Pay" && frm.doc.party_type == "Supplier") || - (frm.doc.payment_type == "Pay" && frm.doc.party_type == "Employee") - ) { - if (total_positive_outstanding_including_order > paid_amount) { - var remaining_outstanding = total_positive_outstanding_including_order - paid_amount; - allocated_negative_outstanding = - total_negative_outstanding < remaining_outstanding - ? total_negative_outstanding - : remaining_outstanding; - } - - var allocated_positive_outstanding = paid_amount + allocated_negative_outstanding; - } else if (["Customer", "Supplier"].includes(frm.doc.party_type)) { - total_negative_outstanding = flt(total_negative_outstanding, precision("outstanding_amount")); - if (paid_amount > total_negative_outstanding) { - if (total_negative_outstanding == 0) { - frappe.msgprint( - __("Cannot {0} {1} {2} without any negative outstanding invoice", [ - frm.doc.payment_type, - frm.doc.party_type == "Customer" ? "to" : "from", - frm.doc.party_type, - ]) - ); - return false; - } else { - frappe.msgprint( - __("Paid Amount cannot be greater than total negative outstanding amount {0}", [ - total_negative_outstanding, - ]) - ); - return false; - } - } else { - allocated_positive_outstanding = total_negative_outstanding - paid_amount; - allocated_negative_outstanding = - paid_amount + - (total_positive_outstanding_including_order < allocated_positive_outstanding - ? total_positive_outstanding_including_order - : allocated_positive_outstanding); - } - } - - $.each(frm.doc.references || [], function (i, row) { - if (frappe.flags.allocate_payment_amount == 0) { - //If allocate payment amount checkbox is unchecked, set zero to allocate amount - row.allocated_amount = 0; - } else if ( - frappe.flags.allocate_payment_amount != 0 && - (!row.allocated_amount || paid_amount_change) - ) { - if (row.outstanding_amount > 0 && allocated_positive_outstanding >= 0) { - row.allocated_amount = - row.outstanding_amount >= allocated_positive_outstanding - ? allocated_positive_outstanding - : row.outstanding_amount; - allocated_positive_outstanding -= flt(row.allocated_amount); - } else if (row.outstanding_amount < 0 && allocated_negative_outstanding) { - row.allocated_amount = - Math.abs(row.outstanding_amount) >= allocated_negative_outstanding - ? -1 * allocated_negative_outstanding - : row.outstanding_amount; - allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount)); - } - } + allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) { + await frm.call("allocate_amount_to_references", { + paid_amount: paid_amount, + paid_amount_change: paid_amount_change, + allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false, }); - frm.refresh_fields(); frm.events.set_total_allocated_amount(frm); }, @@ -1145,36 +1114,34 @@ frappe.ui.form.on("Payment Entry", { }, set_unallocated_amount: function (frm) { - var unallocated_amount = 0; - var total_deductions = frappe.utils.sum( - $.map(frm.doc.deductions || [], function (d) { - return flt(d.amount); - }) - ); + let unallocated_amount = 0; + let deductions_to_consider = 0; + + for (const row of frm.doc.deductions || []) { + if (!row.is_exchange_gain_loss) deductions_to_consider += flt(row.amount); + } + const included_taxes = get_included_taxes(frm); if (frm.doc.party) { if ( frm.doc.payment_type == "Receive" && - frm.doc.base_total_allocated_amount < frm.doc.base_received_amount + total_deductions && - frm.doc.total_allocated_amount < - frm.doc.paid_amount + total_deductions / frm.doc.source_exchange_rate + frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount + deductions_to_consider ) { unallocated_amount = - (frm.doc.base_received_amount + - total_deductions - - flt(frm.doc.base_total_taxes_and_charges) - - frm.doc.base_total_allocated_amount) / + (frm.doc.base_paid_amount + + deductions_to_consider - + frm.doc.base_total_allocated_amount - + included_taxes) / frm.doc.source_exchange_rate; } else if ( frm.doc.payment_type == "Pay" && - frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount - total_deductions && - frm.doc.total_allocated_amount < - frm.doc.received_amount + total_deductions / frm.doc.target_exchange_rate + frm.doc.base_total_allocated_amount < frm.doc.base_received_amount - deductions_to_consider ) { unallocated_amount = - (frm.doc.base_paid_amount + - flt(frm.doc.base_total_taxes_and_charges) - - (total_deductions + frm.doc.base_total_allocated_amount)) / + (frm.doc.base_received_amount - + deductions_to_consider - + frm.doc.base_total_allocated_amount - + included_taxes) / frm.doc.target_exchange_rate; } } @@ -1268,77 +1235,85 @@ frappe.ui.form.on("Payment Entry", { }, write_off_difference_amount: function (frm) { - frm.events.set_deductions_entry(frm, "write_off_account"); + frm.events.set_write_off_deduction(frm); }, - set_exchange_gain_loss: function (frm) { - frm.events.set_deductions_entry(frm, "exchange_gain_loss_account"); + base_paid_amount: function (frm) { + frm.events.set_exchange_gain_loss_deduction(frm); }, - set_deductions_entry: function (frm, account) { - if (frm.doc.difference_amount) { - frappe.call({ - method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_company_defaults", - args: { - company: frm.doc.company, - }, - callback: function (r, rt) { - if (r.message) { - const write_off_row = $.map(frm.doc["deductions"] || [], function (t) { - return t.account == r.message[account] ? t : null; - }); + base_received_amount: function (frm) { + frm.events.set_exchange_gain_loss_deduction(frm); + }, - const difference_amount = flt( - frm.doc.difference_amount, - precision("difference_amount") - ); - - const add_deductions = (details) => { - let row = null; - if (!write_off_row.length && difference_amount) { - row = frm.add_child("deductions"); - row.account = details[account]; - row.cost_center = details["cost_center"]; - } else { - row = write_off_row[0]; - } + set_exchange_gain_loss_deduction: async function (frm) { + // wait for allocate_party_amount_against_ref_docs to finish + await frappe.after_ajax(); + const base_paid_amount = frm.doc.base_paid_amount || 0; + const base_received_amount = frm.doc.base_received_amount || 0; + const exchange_gain_loss = flt( + base_paid_amount - base_received_amount, + get_deduction_amount_precision() + ); - if (row) { - row.amount = flt(row.amount) + difference_amount; - } else { - frappe.msgprint(__("No gain or loss in the exchange rate")); - } - refresh_field("deductions"); - }; - - if (!r.message[account]) { - frappe.prompt( - { - label: __("Please Specify Account"), - fieldname: account, - fieldtype: "Link", - options: "Account", - get_query: () => ({ - filters: { - company: frm.doc.company, - }, - }), - }, - (values) => { - const details = Object.assign({}, r.message, values); - add_deductions(details); - }, - __(frappe.unscrub(account)) - ); - } else { - add_deductions(r.message); - } + if (!exchange_gain_loss) { + frm.events.delete_exchange_gain_loss(frm); + return; + } - frm.events.set_unallocated_amount(frm); - } - }, - }); + const account_fieldname = "exchange_gain_loss_account"; + let row = (frm.doc.deductions || []).find((t) => t.is_exchange_gain_loss); + + if (!row) { + const response = await get_company_defaults(frm.doc.company); + + const account = + response.message?.[account_fieldname] || + (await prompt_for_missing_account(frm, account_fieldname)); + + row = frm.add_child("deductions"); + row.account = account; + row.cost_center = response.message?.cost_center; + row.is_exchange_gain_loss = 1; } + + row.amount = exchange_gain_loss; + frm.refresh_field("deductions"); + frm.events.set_unallocated_amount(frm); + }, + + delete_exchange_gain_loss: function (frm) { + const exchange_gain_loss_row = (frm.doc.deductions || []).find((row) => row.is_exchange_gain_loss); + + if (!exchange_gain_loss_row) return; + + exchange_gain_loss_row.amount = 0; + frm.get_field("deductions").grid.grid_rows[exchange_gain_loss_row.idx - 1].remove(); + frm.refresh_field("deductions"); + }, + + set_write_off_deduction: async function (frm) { + const difference_amount = flt(frm.doc.difference_amount, get_deduction_amount_precision()); + if (!difference_amount) return; + + const account_fieldname = "write_off_account"; + const response = await get_company_defaults(frm.doc.company); + const write_off_account = + response.message?.[account_fieldname] || + (await prompt_for_missing_account(frm, account_fieldname)); + + if (!write_off_account) return; + + let row = (frm.doc["deductions"] || []).find((t) => t.account == write_off_account); + if (!row) { + row = frm.add_child("deductions"); + row.account = write_off_account; + row.cost_center = response.message?.cost_center; + } + + row.amount = flt(row.amount) + difference_amount; + frm.refresh_field("deductions"); + frm.events.set_unallocated_amount(frm); }, bank_account: function (frm) { @@ -1664,6 +1639,62 @@ frappe.ui.form.on("Payment Entry", { return current_tax_amount; }, + + cost_center: function (frm) { + if (frm.doc.posting_date && (frm.doc.paid_from || frm.doc.paid_to)) { + return frappe.call({ + method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_and_account_balance", + args: { + company: frm.doc.company, + date: frm.doc.posting_date, + paid_from: frm.doc.paid_from, + paid_to: frm.doc.paid_to, + ptype: frm.doc.party_type, + pty: frm.doc.party, + cost_center: frm.doc.cost_center, + }, + callback: function (r, rt) { + if (r.message) { + frappe.run_serially([ + () => { + frm.set_value( + "paid_from_account_balance", + r.message.paid_from_account_balance + ); + frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance); + frm.set_value("party_balance", r.message.party_balance); + }, + ]); + } + }, + }); + } + }, + + after_save: function (frm) { + const { matched_payment_requests } = frappe.last_response; + if (!matched_payment_requests) return; + + const COLUMN_LABEL = [ + [__("Reference DocType"), __("Reference Name"), __("Allocated Amount"), __("Payment Request")], + ]; + + frappe.msgprint({ + title: __("Unset Matched Payment Request"), + message: COLUMN_LABEL.concat(matched_payment_requests), + as_table: true, + wide: true, + primary_action: { + label: __("Allocate Payment Request"), + action() { + frappe.hide_msgprint(); + frm.call("set_matched_payment_requests", { matched_payment_requests }, () => { + frm.dirty(); + }); + }, + }, + }); + }, }); frappe.ui.form.on("Payment Entry Reference", { @@ -1748,6 +1779,13 @@ frappe.ui.form.on("Advance Taxes and Charges", { }); frappe.ui.form.on("Payment Entry Deduction", { + before_deductions_remove: function (doc, cdt, cdn) { + const row = frappe.get_doc(cdt, cdn); + if (row.is_exchange_gain_loss && row.amount) { + frappe.throw(__("Cannot delete Exchange Gain/Loss row")); + } + }, + amount: function (frm) { frm.events.set_unallocated_amount(frm); }, @@ -1756,35 +1794,66 @@ frappe.ui.form.on("Payment Entry Deduction", { frm.events.set_unallocated_amount(frm); }, }); -frappe.ui.form.on("Payment Entry", { - cost_center: function (frm) { - if (frm.doc.posting_date && (frm.doc.paid_from || frm.doc.paid_to)) { - return frappe.call({ - method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_and_account_balance", - args: { - company: frm.doc.company, - date: frm.doc.posting_date, - paid_from: frm.doc.paid_from, - paid_to: frm.doc.paid_to, - ptype: frm.doc.party_type, - pty: frm.doc.party, - cost_center: frm.doc.cost_center, - }, - callback: function (r, rt) { - if (r.message) { - frappe.run_serially([ - () => { - frm.set_value( - "paid_from_account_balance", - r.message.paid_from_account_balance - ); - frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance); - frm.set_value("party_balance", r.message.party_balance); - }, - ]); - } - }, - }); + +function set_default_party_type(frm) { + if (frm.doc.party) return; + + let party_type; + if (frm.doc.payment_type == "Receive") { + party_type = "Customer"; + } else if (frm.doc.payment_type == "Pay") { + party_type = "Supplier"; + } + + if (party_type) frm.set_value("party_type", party_type); +} + +function get_included_taxes(frm) { + let included_taxes = 0; + for (const tax of frm.doc.taxes) { + if (!tax.included_in_paid_amount) continue; + + if (tax.add_deduct_tax == "Add") { + included_taxes += tax.base_tax_amount; + } else { + included_taxes -= tax.base_tax_amount; } - }, -}); + } + + return included_taxes; +} + +function get_company_defaults(company) { + return frappe.call({ + method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_company_defaults", + args: { + company: company, + }, + }); +} + +function prompt_for_missing_account(frm, account) { + return new Promise((resolve) => { + const dialog = frappe.prompt( + { + label: __(frappe.unscrub(account)), + fieldname: account, + fieldtype: "Link", + options: "Account", + get_query: () => ({ + filters: { + company: frm.doc.company, + }, + }), + }, + (values) => resolve(values?.[account]), + __("Please Specify Account") + ); + + dialog.on_hide = () => resolve(""); + }); +} + +function get_deduction_amount_precision() { + return frappe.meta.get_field_precision(frappe.meta.get_field("Payment Entry Deduction", "amount")); +} diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index d420bcca3422..69debbec5c7c 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -56,7 +56,6 @@ "section_break_34", "total_allocated_amount", "base_total_allocated_amount", - "set_exchange_gain_loss", "column_break_36", "unallocated_amount", "difference_amount", @@ -390,11 +389,6 @@ "print_hide": 1, "read_only": 1 }, - { - "fieldname": "set_exchange_gain_loss", - "fieldtype": "Button", - "label": "Set Exchange Gain / Loss" - }, { "fieldname": "column_break_36", "fieldtype": "Column Break" @@ -801,7 +795,7 @@ "table_fieldname": "payment_entries" } ], - "modified": "2024-05-31 17:07:06.197249", + "modified": "2024-11-07 11:19:19.320883", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 454101027e7f..7e3d8a5833bb 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -7,8 +7,10 @@ import frappe from frappe import ValidationError, _, qb, scrub, throw +from frappe.query_builder import Tuple +from frappe.query_builder.functions import Count from frappe.utils import cint, comma_or, flt, getdate, nowdate -from frappe.utils.data import comma_and, fmt_money +from frappe.utils.data import comma_and, fmt_money, get_link_to_form from pypika import Case from pypika.functions import Coalesce, Sum @@ -98,13 +100,18 @@ def validate(self): self.set_status() self.set_total_in_words() + def before_save(self): + self.set_matched_unset_payment_requests_to_response() + def on_submit(self): if self.difference_amount: frappe.throw(_("Difference Amount must be zero")) self.make_gl_entries() self.update_outstanding_amounts() - self.update_advance_paid() self.update_payment_schedule() + self.update_payment_requests() + self.make_advance_payment_ledger_entries() + self.update_advance_paid() # advance_paid_status depends on the payment request amount self.set_status() def set_liability_account(self): @@ -145,9 +152,21 @@ def set_liability_account(self): self.is_opening = "No" return - liability_account = get_party_account( - self.party_type, self.party, self.company, include_advance=True - )[1] + accounts = get_party_account(self.party_type, self.party, self.company, include_advance=True) + + liability_account = accounts[1] if len(accounts) > 1 else None + fieldname = ( + "default_advance_received_account" + if self.party_type == "Customer" + else "default_advance_paid_account" + ) + + if not liability_account: + throw( + _("Please set default {0} in Company {1}").format( + frappe.bold(frappe.get_meta("Company").get_label(fieldname)), frappe.bold(self.company) + ) + ) self.set(self.party_account_field, liability_account) @@ -172,34 +191,40 @@ def on_cancel(self): "Repost Accounting Ledger Items", "Unreconcile Payment", "Unreconcile Payment Entries", + "Advance Payment Ledger Entry", ) super().on_cancel() self.make_gl_entries(cancel=1) self.update_outstanding_amounts() - self.update_advance_paid() self.delink_advance_entry_references() self.update_payment_schedule(cancel=1) - self.set_payment_req_status() + self.update_payment_requests(cancel=True) + self.make_advance_payment_ledger_entries() + self.update_advance_paid() # advance_paid_status depends on the payment request amount self.set_status() - def set_payment_req_status(self): - from erpnext.accounts.doctype.payment_request.payment_request import update_payment_req_status + def update_payment_requests(self, cancel=False): + from erpnext.accounts.doctype.payment_request.payment_request import ( + update_payment_requests_as_per_pe_references, + ) - update_payment_req_status(self, None) + update_payment_requests_as_per_pe_references(self.references, cancel=cancel) def update_outstanding_amounts(self): self.set_missing_ref_details(force=True) def validate_duplicate_entry(self): - reference_names = [] + reference_names = set() for d in self.get("references"): - if (d.reference_doctype, d.reference_name, d.payment_term) in reference_names: + key = (d.reference_doctype, d.reference_name, d.payment_term, d.payment_request) + if key in reference_names: frappe.throw( _("Row #{0}: Duplicate entry in References {1} {2}").format( d.idx, d.reference_doctype, d.reference_name ) ) - reference_names.append((d.reference_doctype, d.reference_name, d.payment_term)) + + reference_names.add(key) def set_bank_account_data(self): if self.bank_account: @@ -225,6 +250,8 @@ def validate_allocated_amount(self): if self.payment_type == "Internal Transfer": return + self.validate_allocated_amount_as_per_payment_request() + if self.party_type in ("Customer", "Supplier"): self.validate_allocated_amount_with_latest_data() else: @@ -237,6 +264,27 @@ def validate_allocated_amount(self): if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount): frappe.throw(fail_message.format(d.idx)) + def validate_allocated_amount_as_per_payment_request(self): + """ + Allocated amount should not be greater than the outstanding amount of the Payment Request. + """ + if not self.references: + return + + pr_outstanding_amounts = get_payment_request_outstanding_set_in_references(self.references) + + if not pr_outstanding_amounts: + return + + for ref in self.references: + if ref.payment_request and ref.allocated_amount > pr_outstanding_amounts[ref.payment_request]: + frappe.throw( + msg=_( + "Row #{0}: Allocated Amount cannot be greater than Outstanding Amount of Payment Request {1}" + ).format(ref.idx, get_link_to_form("Payment Request", ref.payment_request)), + title=_("Invalid Allocated Amount"), + ) + def term_based_allocation_enabled_for_reference( self, reference_doctype: str, reference_name: str ) -> bool: @@ -845,6 +893,7 @@ def set_amounts(self): self.set_amounts_in_company_currency() self.set_total_allocated_amount() self.set_unallocated_amount() + self.set_exchange_gain_loss() self.set_difference_amount() def validate_amounts(self): @@ -940,10 +989,10 @@ def calculate_base_allocated_amount_for_reference(self, d) -> float: if d.exchange_rate is None: d.exchange_rate = 1 - allocated_amount_in_pe_exchange_rate = flt( + allocated_amount_in_ref_exchange_rate = flt( flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount") ) - d.exchange_gain_loss = base_allocated_amount - allocated_amount_in_pe_exchange_rate + d.exchange_gain_loss = base_allocated_amount - allocated_amount_in_ref_exchange_rate return base_allocated_amount def set_total_allocated_amount(self): @@ -961,29 +1010,80 @@ def set_total_allocated_amount(self): def set_unallocated_amount(self): self.unallocated_amount = 0 - if self.party: - total_deductions = sum(flt(d.amount) for d in self.get("deductions")) - included_taxes = self.get_included_taxes() - if ( - self.payment_type == "Receive" - and self.base_total_allocated_amount < self.base_received_amount + total_deductions - and self.total_allocated_amount - < flt(self.paid_amount) + (total_deductions / self.source_exchange_rate) - ): - self.unallocated_amount = ( - self.base_received_amount + total_deductions - self.base_total_allocated_amount - ) / self.source_exchange_rate - self.unallocated_amount -= included_taxes - elif ( - self.payment_type == "Pay" - and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) - and self.total_allocated_amount - < flt(self.received_amount) + (total_deductions / self.target_exchange_rate) - ): - self.unallocated_amount = ( - self.base_paid_amount - (total_deductions + self.base_total_allocated_amount) - ) / self.target_exchange_rate - self.unallocated_amount -= included_taxes + if not self.party: + return + + deductions_to_consider = sum( + flt(d.amount) for d in self.get("deductions") if not d.is_exchange_gain_loss + ) + included_taxes = self.get_included_taxes() + + if self.payment_type == "Receive" and self.base_total_allocated_amount < ( + self.base_paid_amount + deductions_to_consider + ): + self.unallocated_amount = ( + self.base_paid_amount + + deductions_to_consider + - self.base_total_allocated_amount + - included_taxes + ) / self.source_exchange_rate + elif self.payment_type == "Pay" and self.base_total_allocated_amount < ( + self.base_received_amount - deductions_to_consider + ): + self.unallocated_amount = ( + self.base_received_amount + - deductions_to_consider + - self.base_total_allocated_amount + - included_taxes + ) / self.target_exchange_rate + + def set_exchange_gain_loss(self): + exchange_gain_loss = flt( + self.base_paid_amount - self.base_received_amount, + self.precision("amount", "deductions"), + ) + + exchange_gain_loss_rows = [row for row in self.get("deductions") if row.is_exchange_gain_loss] + exchange_gain_loss_row = exchange_gain_loss_rows.pop(0) if exchange_gain_loss_rows else None + + for row in exchange_gain_loss_rows: + self.remove(row) + + if not exchange_gain_loss: + if exchange_gain_loss_row: + self.remove(exchange_gain_loss_row) + + return + + if not exchange_gain_loss_row: + values = frappe.get_cached_value( + "Company", self.company, ("exchange_gain_loss_account", "cost_center"), as_dict=True + ) + + for fieldname, value in values.items(): + if value: + continue + + label = _(frappe.get_meta("Company").get_label(fieldname)) + return frappe.msgprint( + _("Please set {0} in Company {1} to account for Exchange Gain / Loss").format( + label, get_link_to_form("Company", self.company) + ), + title=_("Missing Default in Company"), + indicator="red" if self.docstatus.is_submitted() else "yellow", + raise_exception=self.docstatus.is_submitted(), + ) + + exchange_gain_loss_row = self.append( + "deductions", + { + "account": values.exchange_gain_loss_account, + "cost_center": values.cost_center, + "is_exchange_gain_loss": 1, + }, + ) + + exchange_gain_loss_row.amount = exchange_gain_loss def set_difference_amount(self): base_unallocated_amount = flt(self.unallocated_amount) * ( @@ -1011,11 +1111,13 @@ def set_difference_amount(self): def get_included_taxes(self): included_taxes = 0 for tax in self.get("taxes"): - if tax.included_in_paid_amount: - if tax.add_deduct_tax == "Add": - included_taxes += tax.base_tax_amount - else: - included_taxes -= tax.base_tax_amount + if not tax.included_in_paid_amount: + continue + + if tax.add_deduct_tax == "Add": + included_taxes += tax.base_tax_amount + else: + included_taxes -= tax.base_tax_amount return included_taxes @@ -1098,6 +1200,12 @@ def build_gl_map(self): if self.payment_type in ("Receive", "Pay") and not self.get("party_account_field"): self.setup_party_account_field() + company_currency = erpnext.get_company_currency(self.company) + if self.paid_from_account_currency != company_currency: + self.currency = self.paid_from_account_currency + elif self.paid_to_account_currency != company_currency: + self.currency = self.paid_to_account_currency + gl_entries = [] self.add_party_gl_entries(gl_entries) self.add_bank_gl_entries(gl_entries) @@ -1121,6 +1229,8 @@ def add_party_gl_entries(self, gl_entries): if not self.party_account: return + advance_payment_doctypes = frappe.get_hooks("advance_payment_doctypes") + if self.payment_type == "Receive": against_account = self.paid_to else: @@ -1163,14 +1273,41 @@ def add_party_gl_entries(self, gl_entries): dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" gle.update( - { - dr_or_cr: allocated_amount_in_company_currency, - dr_or_cr + "_in_account_currency": d.allocated_amount, - "against_voucher_type": d.reference_doctype, - "against_voucher": d.reference_name, - "cost_center": cost_center, - } + self.get_gl_dict( + { + "account": self.party_account, + "party_type": self.party_type, + "party": self.party, + "against": against_account, + "account_currency": self.party_account_currency, + "cost_center": cost_center, + dr_or_cr + "_in_account_currency": d.allocated_amount, + dr_or_cr: allocated_amount_in_company_currency, + }, + item=self, + ) ) + + if self.book_advance_payments_in_separate_party_account: + if d.reference_doctype in advance_payment_doctypes: + # Upon reconciliation, whole ledger will be reposted. So, reference to SO/PO is fine + gle.update( + { + "against_voucher_type": d.reference_doctype, + "against_voucher": d.reference_name, + } + ) + else: + # Do not reference Invoices while Advance is in separate party account + gle.update({"against_voucher_type": self.doctype, "against_voucher": self.name}) + else: + gle.update( + { + "against_voucher_type": d.reference_doctype, + "against_voucher": d.reference_name, + } + ) + gl_entries.append(gle) if self.unallocated_amount: @@ -1179,13 +1316,22 @@ def add_party_gl_entries(self, gl_entries): base_unallocated_amount = self.unallocated_amount * exchange_rate gle = party_gl_dict.copy() + gle.update( - { - dr_or_cr + "_in_account_currency": self.unallocated_amount, - dr_or_cr: base_unallocated_amount, - } + self.get_gl_dict( + { + "account": self.party_account, + "party_type": self.party_type, + "party": self.party, + "against": against_account, + "account_currency": self.party_account_currency, + "cost_center": self.cost_center, + dr_or_cr + "_in_account_currency": self.unallocated_amount, + dr_or_cr: base_unallocated_amount, + }, + item=self, + ) ) - if self.book_advance_payments_in_separate_party_account: gle.update( { @@ -1594,6 +1740,380 @@ def get_current_tax_fraction(self, tax): return current_tax_fraction + def set_matched_unset_payment_requests_to_response(self): + """ + Find matched Payment Requests for those references which have no Payment Request set.\n + And set to `frappe.response` to show in the frontend for allocation. + """ + if not self.references: + return + + matched_payment_requests = get_matched_payment_request_of_references( + [row for row in self.references if not row.payment_request] + ) + + if not matched_payment_requests: + return + + frappe.response["matched_payment_requests"] = matched_payment_requests + + @frappe.whitelist() + def allocate_amount_to_references(self, paid_amount, paid_amount_change, allocate_payment_amount): + """ + Allocate `Allocated Amount` and `Payment Request` against `Reference` based on `Paid Amount` and `Outstanding Amount`.\n + :param paid_amount: Paid Amount / Received Amount. + :param paid_amount_change: Flag to check if `Paid Amount` is changed or not. + :param allocate_payment_amount: Flag to allocate amount or not. (Payment Request is also dependent on this flag) + """ + if not self.references: + return + + if not allocate_payment_amount: + for ref in self.references: + ref.allocated_amount = 0 + return + + # calculating outstanding amounts + precision = self.precision("paid_amount") + total_positive_outstanding_including_order = 0 + total_negative_outstanding = 0 + paid_amount -= sum(flt(d.amount, precision) for d in self.deductions) + + for ref in self.references: + reference_outstanding_amount = ref.outstanding_amount + abs_outstanding_amount = abs(reference_outstanding_amount) + + if reference_outstanding_amount > 0: + total_positive_outstanding_including_order += abs_outstanding_amount + else: + total_negative_outstanding += abs_outstanding_amount + + # calculating allocated outstanding amounts + allocated_negative_outstanding = 0 + allocated_positive_outstanding = 0 + + # checking party type and payment type + if (self.payment_type == "Receive" and self.party_type == "Customer") or ( + self.payment_type == "Pay" and self.party_type in ("Supplier", "Employee") + ): + if total_positive_outstanding_including_order > paid_amount: + remaining_outstanding = flt( + total_positive_outstanding_including_order - paid_amount, precision + ) + allocated_negative_outstanding = min(remaining_outstanding, total_negative_outstanding) + + allocated_positive_outstanding = paid_amount + allocated_negative_outstanding + + elif self.party_type in ("Supplier", "Employee"): + if paid_amount > total_negative_outstanding: + if total_negative_outstanding == 0: + frappe.msgprint( + _("Cannot {0} from {1} without any negative outstanding invoice").format( + self.payment_type, + self.party_type, + ) + ) + else: + frappe.msgprint( + _("Paid Amount cannot be greater than total negative outstanding amount {0}").format( + total_negative_outstanding + ) + ) + + return + + else: + allocated_positive_outstanding = flt(total_negative_outstanding - paid_amount, precision) + allocated_negative_outstanding = paid_amount + min( + total_positive_outstanding_including_order, allocated_positive_outstanding + ) + + # inner function to set `allocated_amount` to those row which have no PR + def _allocation_to_unset_pr_row( + row, outstanding_amount, allocated_positive_outstanding, allocated_negative_outstanding + ): + if outstanding_amount > 0 and allocated_positive_outstanding >= 0: + row.allocated_amount = min(allocated_positive_outstanding, outstanding_amount) + allocated_positive_outstanding = flt( + allocated_positive_outstanding - row.allocated_amount, precision + ) + elif outstanding_amount < 0 and allocated_negative_outstanding: + row.allocated_amount = min(allocated_negative_outstanding, abs(outstanding_amount)) * -1 + allocated_negative_outstanding = flt( + allocated_negative_outstanding - abs(row.allocated_amount), precision + ) + return allocated_positive_outstanding, allocated_negative_outstanding + + # allocate amount based on `paid_amount` is changed or not + if not paid_amount_change: + for ref in self.references: + allocated_positive_outstanding, allocated_negative_outstanding = _allocation_to_unset_pr_row( + ref, + ref.outstanding_amount, + allocated_positive_outstanding, + allocated_negative_outstanding, + ) + + allocate_open_payment_requests_to_references(self.references, self.precision("paid_amount")) + + else: + payment_request_outstanding_amounts = ( + get_payment_request_outstanding_set_in_references(self.references) or {} + ) + references_outstanding_amounts = get_references_outstanding_amount(self.references) or {} + remaining_references_allocated_amounts = references_outstanding_amounts.copy() + + # Re allocate amount to those references which have PR set (Higher priority) + for ref in self.references: + if not ref.payment_request: + continue + + # fetch outstanding_amount of `Reference` (Payment Term) and `Payment Request` to allocate new amount + key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term")) + reference_outstanding_amount = references_outstanding_amounts[key] + pr_outstanding_amount = payment_request_outstanding_amounts[ref.payment_request] + + if reference_outstanding_amount > 0 and allocated_positive_outstanding >= 0: + # allocate amount according to outstanding amounts + outstanding_amounts = ( + allocated_positive_outstanding, + reference_outstanding_amount, + pr_outstanding_amount, + ) + + ref.allocated_amount = min(outstanding_amounts) + + # update amounts to track allocation + allocated_amount = ref.allocated_amount + allocated_positive_outstanding = flt( + allocated_positive_outstanding - allocated_amount, precision + ) + remaining_references_allocated_amounts[key] = flt( + remaining_references_allocated_amounts[key] - allocated_amount, precision + ) + payment_request_outstanding_amounts[ref.payment_request] = flt( + payment_request_outstanding_amounts[ref.payment_request] - allocated_amount, precision + ) + + elif reference_outstanding_amount < 0 and allocated_negative_outstanding: + # allocate amount according to outstanding amounts + outstanding_amounts = ( + allocated_negative_outstanding, + abs(reference_outstanding_amount), + pr_outstanding_amount, + ) + + ref.allocated_amount = min(outstanding_amounts) * -1 + + # update amounts to track allocation + allocated_amount = abs(ref.allocated_amount) + allocated_negative_outstanding = flt( + allocated_negative_outstanding - allocated_amount, precision + ) + remaining_references_allocated_amounts[key] += allocated_amount # negative amount + payment_request_outstanding_amounts[ref.payment_request] = flt( + payment_request_outstanding_amounts[ref.payment_request] - allocated_amount, precision + ) + # Re allocate amount to those references which have no PR (Lower priority) + for ref in self.references: + if ref.payment_request: + continue + + key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term")) + reference_outstanding_amount = remaining_references_allocated_amounts[key] + + allocated_positive_outstanding, allocated_negative_outstanding = _allocation_to_unset_pr_row( + ref, + reference_outstanding_amount, + allocated_positive_outstanding, + allocated_negative_outstanding, + ) + + @frappe.whitelist() + def set_matched_payment_requests(self, matched_payment_requests): + """ + Set `Payment Request` against `Reference` based on `matched_payment_requests`.\n + :param matched_payment_requests: List of tuple of matched Payment Requests. + + --- + Example: [(reference_doctype, reference_name, allocated_amount, payment_request), ...] + """ + if not self.references or not matched_payment_requests: + return + + if isinstance(matched_payment_requests, str): + matched_payment_requests = json.loads(matched_payment_requests) + + # modify matched_payment_requests + # like (reference_doctype, reference_name, allocated_amount): payment_request + payment_requests = {} + + for row in matched_payment_requests: + key = tuple(row[:3]) + payment_requests[key] = row[3] + + for ref in self.references: + if ref.payment_request: + continue + + key = (ref.reference_doctype, ref.reference_name, ref.allocated_amount) + + if key in payment_requests: + ref.payment_request = payment_requests[key] + del payment_requests[key] # to avoid duplicate allocation + + +def get_matched_payment_request_of_references(references=None): + """ + Get those `Payment Requests` which are matched with `References`.\n + - Amount must be same. + - Only single `Payment Request` available for this amount. + + Example: [(reference_doctype, reference_name, allocated_amount, payment_request), ...] + """ + if not references: + return + + # to fetch matched rows + refs = { + (row.reference_doctype, row.reference_name, row.allocated_amount) + for row in references + if row.reference_doctype and row.reference_name and row.allocated_amount + } + + if not refs: + return + + PR = frappe.qb.DocType("Payment Request") + + # query to group by reference_doctype, reference_name, outstanding_amount + subquery = ( + frappe.qb.from_(PR) + .select( + PR.reference_doctype, + PR.reference_name, + PR.outstanding_amount.as_("allocated_amount"), + PR.name.as_("payment_request"), + Count("*").as_("count"), + ) + .where(Tuple(PR.reference_doctype, PR.reference_name, PR.outstanding_amount).isin(refs)) + .where(PR.status != "Paid") + .where(PR.docstatus == 1) + .groupby(PR.reference_doctype, PR.reference_name, PR.outstanding_amount) + ) + + # query to fetch matched rows which are single + matched_prs = ( + frappe.qb.from_(subquery) + .select( + subquery.reference_doctype, + subquery.reference_name, + subquery.allocated_amount, + subquery.payment_request, + ) + .where(subquery.count == 1) + .run() + ) + + return matched_prs if matched_prs else None + + +def get_references_outstanding_amount(references=None): + """ + Fetch accurate outstanding amount of `References`.\n + - If `Payment Term` is set, then fetch outstanding amount from `Payment Schedule`. + - If `Payment Term` is not set, then fetch outstanding amount from `References` it self. + + Example: {(reference_doctype, reference_name, payment_term): outstanding_amount, ...} + """ + if not references: + return + + refs_with_payment_term = get_outstanding_of_references_with_payment_term(references) or {} + refs_without_payment_term = get_outstanding_of_references_with_no_payment_term(references) or {} + + return {**refs_with_payment_term, **refs_without_payment_term} + + +def get_outstanding_of_references_with_payment_term(references=None): + """ + Fetch outstanding amount of `References` which have `Payment Term` set.\n + Example: {(reference_doctype, reference_name, payment_term): outstanding_amount, ...} + """ + if not references: + return + + refs = { + (row.reference_doctype, row.reference_name, row.payment_term) + for row in references + if row.reference_doctype and row.reference_name and row.payment_term + } + + if not refs: + return + + PS = frappe.qb.DocType("Payment Schedule") + + response = ( + frappe.qb.from_(PS) + .select(PS.parenttype, PS.parent, PS.payment_term, PS.outstanding) + .where(Tuple(PS.parenttype, PS.parent, PS.payment_term).isin(refs)) + ).run(as_dict=True) + + if not response: + return + + return {(row.parenttype, row.parent, row.payment_term): row.outstanding for row in response} + + +def get_outstanding_of_references_with_no_payment_term(references): + """ + Fetch outstanding amount of `References` which have no `Payment Term` set.\n + - Fetch outstanding amount from `References` it self. + + Note: `None` is used for allocation of `Payment Request` + Example: {(reference_doctype, reference_name, None): outstanding_amount, ...} + """ + if not references: + return + + outstanding_amounts = {} + + for ref in references: + if ref.payment_term: + continue + + key = (ref.reference_doctype, ref.reference_name, None) + + if key not in outstanding_amounts: + outstanding_amounts[key] = ref.outstanding_amount + + return outstanding_amounts + + +def get_payment_request_outstanding_set_in_references(references=None): + """ + Fetch outstanding amount of `Payment Request` which are set in `References`.\n + Example: {payment_request: outstanding_amount, ...} + """ + if not references: + return + + referenced_payment_requests = {row.payment_request for row in references if row.payment_request} + + if not referenced_payment_requests: + return + + PR = frappe.qb.DocType("Payment Request") + + response = ( + frappe.qb.from_(PR) + .select(PR.name, PR.outstanding_amount) + .where(PR.name.isin(referenced_payment_requests)) + ).run() + + return dict(response) if response else None + def validate_inclusive_tax(tax, doc): def _on_previous_row_error(row_range): @@ -1740,7 +2260,7 @@ def get_outstanding_reference_documents(args, validate=False): d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no") # Get negative outstanding sales /purchase invoices - if args.get("party_type") != "Employee" and not args.get("voucher_no"): + if args.get("party_type") != "Employee": negative_outstanding_invoices = get_negative_outstanding_invoices( args.get("party_type"), args.get("party"), @@ -2046,7 +2566,9 @@ def get_party_details(company, party_type, party, date, cost_center=None): account_balance = get_balance_on(party_account, date, cost_center=cost_center) _party_name = "title" if party_type == "Shareholder" else party_type.lower() + "_name" party_name = frappe.db.get_value(party_type, party, _party_name) - party_balance = get_balance_on(party_type=party_type, party=party, cost_center=cost_center) + party_balance = get_balance_on( + party_type=party_type, party=party, company=company, cost_center=cost_center + ) if party_type in ["Customer", "Supplier"]: party_bank_account = get_party_bank_account(party_type, party) bank_account = get_default_company_bank_account(company, party_type, party) @@ -2224,6 +2746,8 @@ def get_payment_entry( party_type=None, payment_type=None, reference_date=None, + ignore_permissions=False, + created_from_payment_request=False, ): doc = frappe.get_doc(dt, dn) over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance") @@ -2359,9 +2883,6 @@ def get_payment_entry( update_accounting_dimensions(pe, doc) if party_account and bank: - pe.set_exchange_rate(ref_doc=doc) - pe.set_amounts() - if discount_amount: base_total_discount_loss = 0 if frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss"): @@ -2371,11 +2892,183 @@ def get_payment_entry( pe, doc, discount_amount, base_total_discount_loss, party_account_currency ) - pe.set_difference_amount() + pe.set_exchange_rate(ref_doc=doc) + pe.set_amounts() + + # If PE is created from PR directly, then no need to find open PRs for the references + if not created_from_payment_request: + allocate_open_payment_requests_to_references(pe.references, pe.precision("paid_amount")) return pe +def get_open_payment_requests_for_references(references=None): + """ + Fetch all unpaid Payment Requests for the references. \n + - Each reference can have multiple Payment Requests. \n + + Example: {("Sales Invoice", "SINV-00001"): {"PREQ-00001": 1000, "PREQ-00002": 2000}} + """ + if not references: + return + + refs = { + (row.reference_doctype, row.reference_name) + for row in references + if row.reference_doctype and row.reference_name and row.allocated_amount + } + + if not refs: + return + + PR = frappe.qb.DocType("Payment Request") + + response = ( + frappe.qb.from_(PR) + .select(PR.name, PR.reference_doctype, PR.reference_name, PR.outstanding_amount) + .where(Tuple(PR.reference_doctype, PR.reference_name).isin(list(refs))) + .where(PR.status != "Paid") + .where(PR.docstatus == 1) + .where(PR.outstanding_amount > 0) # to avoid old PRs with 0 outstanding amount + .orderby(Coalesce(PR.transaction_date, PR.creation), order=frappe.qb.asc) + ).run(as_dict=True) + + if not response: + return + + reference_payment_requests = {} + + for row in response: + key = (row.reference_doctype, row.reference_name) + + if key not in reference_payment_requests: + reference_payment_requests[key] = {row.name: row.outstanding_amount} + else: + reference_payment_requests[key][row.name] = row.outstanding_amount + + return reference_payment_requests + + +def allocate_open_payment_requests_to_references(references=None, precision=None): + """ + Allocate unpaid Payment Requests to the references. \n + --- + - Allocation based on below factors + - Reference Allocated Amount + - Reference Outstanding Amount (With Payment Terms or without Payment Terms) + - Reference Payment Request's outstanding amount + --- + - Allocation based on below scenarios + - Reference's Allocated Amount == Payment Request's Outstanding Amount + - Allocate the Payment Request to the reference + - This PR will not be allocated further + - Reference's Allocated Amount < Payment Request's Outstanding Amount + - Allocate the Payment Request to the reference + - Reduce the PR's outstanding amount by the allocated amount + - This PR can be allocated further + - Reference's Allocated Amount > Payment Request's Outstanding Amount + - Allocate the Payment Request to the reference + - Reduce Allocated Amount of the reference by the PR's outstanding amount + - Create a new row for the remaining amount until the Allocated Amount is 0 + - Allocate PR if available + --- + - Note: + - Priority is given to the first Payment Request of respective references. + - Single Reference can have multiple rows. + - With Payment Terms or without Payment Terms + - With Payment Request or without Payment Request + """ + if not references: + return + + # get all unpaid payment requests for the references + references_open_payment_requests = get_open_payment_requests_for_references(references) + + if not references_open_payment_requests: + return + + if not precision: + precision = references[0].precision("allocated_amount") + + # to manage new rows + row_number = 1 + MOVE_TO_NEXT_ROW = 1 + TO_SKIP_NEW_ROW = 2 + + while row_number <= len(references): + row = references[row_number - 1] + reference_key = (row.reference_doctype, row.reference_name) + + # update the idx to maintain the order + row.idx = row_number + + # unpaid payment requests for the reference + reference_payment_requests = references_open_payment_requests.get(reference_key) + + if not reference_payment_requests: + row_number += MOVE_TO_NEXT_ROW # to move to next reference row + continue + + # get the first payment request and its outstanding amount + payment_request, pr_outstanding_amount = next(iter(reference_payment_requests.items())) + allocated_amount = row.allocated_amount + + # allocate the payment request to the reference and PR's outstanding amount + row.payment_request = payment_request + + if pr_outstanding_amount == allocated_amount: + del reference_payment_requests[payment_request] + row_number += MOVE_TO_NEXT_ROW + + elif pr_outstanding_amount > allocated_amount: + # reduce the outstanding amount of the payment request + reference_payment_requests[payment_request] -= allocated_amount + row_number += MOVE_TO_NEXT_ROW + + else: + # split the reference row to allocate the remaining amount + del reference_payment_requests[payment_request] + row.allocated_amount = pr_outstanding_amount + allocated_amount = flt(allocated_amount - pr_outstanding_amount, precision) + + # set the remaining amount to the next row + while allocated_amount: + # create a new row for the remaining amount + new_row = frappe.copy_doc(row) + references.insert(row_number, new_row) + + # get the first payment request and its outstanding amount + payment_request, pr_outstanding_amount = next( + iter(reference_payment_requests.items()), (None, None) + ) + + # update new row + new_row.idx = row_number + 1 + new_row.payment_request = payment_request + new_row.allocated_amount = min( + pr_outstanding_amount if pr_outstanding_amount else allocated_amount, allocated_amount + ) + + if not payment_request or not pr_outstanding_amount: + row_number += TO_SKIP_NEW_ROW + break + + elif pr_outstanding_amount == allocated_amount: + del reference_payment_requests[payment_request] + row_number += TO_SKIP_NEW_ROW + break + + elif pr_outstanding_amount > allocated_amount: + reference_payment_requests[payment_request] -= allocated_amount + row_number += TO_SKIP_NEW_ROW + break + + else: + allocated_amount = flt(allocated_amount - pr_outstanding_amount, precision) + del reference_payment_requests[payment_request] + row_number += MOVE_TO_NEXT_ROW + + def update_accounting_dimensions(pe, doc): """ Updates accounting dimensions in Payment Entry based on the accounting dimensions in the reference document @@ -2547,13 +3240,14 @@ def set_pending_discount_loss(pe, doc, discount_amount, base_total_discount_loss book_tax_loss = frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss") account_type = "round_off_account" if book_tax_loss else "default_discount_account" - pe.set_gain_or_loss( - account_details={ + pe.append( + "deductions", + { "account": frappe.get_cached_value("Company", pe.company, account_type), "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), "amount": discount_amount * positive_negative, - } + }, ) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index cc03dc260bbd..312628d9f97e 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -479,16 +479,9 @@ def test_payment_entry_multicurrency_accounting_si_with_early_payment_discount(s self.assertEqual(pe.deductions[0].account, "Write Off - _TC") # Exchange loss - self.assertEqual(pe.difference_amount, 300.0) - - pe.append( - "deductions", - { - "account": "_Test Exchange Gain/Loss - _TC", - "cost_center": "_Test Cost Center - _TC", - "amount": 300.0, - }, - ) + self.assertEqual(pe.deductions[-1].amount, 300.0) + pe.deductions[-1].account = "_Test Exchange Gain/Loss - _TC" + pe.deductions[-1].cost_center = "_Test Cost Center - _TC" pe.insert() pe.submit() @@ -552,16 +545,10 @@ def test_payment_entry_against_si_usd_to_inr(self): pe.reference_no = "1" pe.reference_date = "2016-01-01" - self.assertEqual(pe.difference_amount, 100) + self.assertEqual(pe.deductions[0].amount, 100) + pe.deductions[0].account = "_Test Exchange Gain/Loss - _TC" + pe.deductions[0].cost_center = "_Test Cost Center - _TC" - pe.append( - "deductions", - { - "account": "_Test Exchange Gain/Loss - _TC", - "cost_center": "_Test Cost Center - _TC", - "amount": 100, - }, - ) pe.insert() pe.submit() @@ -654,16 +641,9 @@ def test_internal_transfer_usd_to_inr(self): pe.set_exchange_rate() pe.set_amounts() - self.assertEqual(pe.difference_amount, 500) - - pe.append( - "deductions", - { - "account": "_Test Exchange Gain/Loss - _TC", - "cost_center": "_Test Cost Center - _TC", - "amount": 500, - }, - ) + self.assertEqual(pe.deductions[0].amount, 500) + pe.deductions[0].account = "_Test Exchange Gain/Loss - _TC" + pe.deductions[0].cost_center = "_Test Cost Center - _TC" pe.insert() pe.submit() @@ -956,6 +936,53 @@ def test_payment_entry_account_and_party_balance_with_cost_center(self): self.assertEqual(flt(expected_party_balance), party_balance) self.assertEqual(flt(expected_party_account_balance, 2), flt(party_account_balance, 2)) + def test_gl_of_multi_currency_payment_transaction(self): + from erpnext.setup.doctype.currency_exchange.test_currency_exchange import ( + save_new_records, + test_records, + ) + + save_new_records(test_records) + paid_from = create_account( + parent_account="Current Liabilities - _TC", + account_name="_Test Cash USD", + company="_Test Company", + account_type="Cash", + account_currency="USD", + ) + payment_entry = create_payment_entry( + party="_Test Supplier USD", + paid_from=paid_from, + paid_to="_Test Payable USD - _TC", + paid_amount=100, + save=True, + ) + payment_entry.source_exchange_rate = 84.4 + payment_entry.target_exchange_rate = 84.4 + payment_entry.save() + payment_entry = payment_entry.submit() + gle = qb.DocType("GL Entry") + gl_entries = ( + qb.from_(gle) + .select( + gle.account, + gle.debit, + gle.credit, + gle.debit_in_account_currency, + gle.credit_in_account_currency, + gle.debit_in_transaction_currency, + gle.credit_in_transaction_currency, + ) + .orderby(gle.account) + .where(gle.voucher_no == payment_entry.name) + .run() + ) + expected_gl_entries = ( + (paid_from, 0.0, 8440.0, 0.0, 100.0, 0.0, 100.0), + ("_Test Payable USD - _TC", 8440.0, 0.0, 100.0, 0.0, 100.0, 0.0), + ) + self.assertEqual(gl_entries, expected_gl_entries) + def test_multi_currency_payment_entry_with_taxes(self): payment_entry = create_payment_entry( party="_Test Supplier USD", paid_to="_Test Payable USD - _TC", save=True @@ -1791,6 +1818,79 @@ def test_opening_flag_for_advance_as_liability(self): # 'Is Opening' should always be 'No' for normal advance payments self.assertEqual(gl_with_opening_set, []) + @change_settings("Accounts Settings", {"delete_linked_ledger_entries": 1}) + def test_delete_linked_exchange_gain_loss_journal(self): + from erpnext.accounts.doctype.account.test_account import create_account + from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import ( + make_customer, + ) + + debtors = create_account( + account_name="Debtors USD", + parent_account="Accounts Receivable - _TC", + company="_Test Company", + account_currency="USD", + account_type="Receivable", + ) + + # create a customer + customer = make_customer(customer="_Test Party USD") + cust_doc = frappe.get_doc("Customer", customer) + cust_doc.default_currency = "USD" + test_account_details = { + "company": "_Test Company", + "account": debtors, + } + cust_doc.append("accounts", test_account_details) + cust_doc.save() + + # create a sales invoice + si = create_sales_invoice( + customer=customer, + currency="USD", + conversion_rate=83.970000000, + debit_to=debtors, + do_not_save=1, + ) + si.party_account_currency = "USD" + si.save() + si.submit() + + # create a payment entry for the invoice + pe = get_payment_entry("Sales Invoice", si.name) + pe.reference_no = "1" + pe.reference_date = frappe.utils.nowdate() + pe.paid_amount = 100 + pe.source_exchange_rate = 90 + pe.append( + "deductions", + { + "account": "_Test Exchange Gain/Loss - _TC", + "cost_center": "_Test Cost Center - _TC", + "amount": 2710, + }, + ) + pe.save() + pe.submit() + + # check creation of journal entry + jv = frappe.get_all( + "Journal Entry Account", + {"reference_type": pe.doctype, "reference_name": pe.name, "docstatus": 1}, + pluck="parent", + ) + self.assertTrue(jv) + + # check cancellation of payment entry and journal entry + pe.cancel() + self.assertTrue(pe.docstatus == 2) + self.assertTrue(frappe.db.get_value("Journal Entry", {"name": jv[0]}, "docstatus") == 2) + + # check deletion of payment entry and journal entry + pe.delete() + self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, pe.doctype, pe.name) + self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, "Journal Entry", jv[0]) + def create_payment_entry(**args): payment_entry = frappe.new_doc("Payment Entry") diff --git a/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json b/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json index 1c31829f0ea6..e47b51ae028f 100644 --- a/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json +++ b/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json @@ -9,6 +9,7 @@ "cost_center", "amount", "column_break_2", + "is_exchange_gain_loss", "description" ], "fields": [ @@ -45,12 +46,20 @@ "fieldname": "description", "fieldtype": "Small Text", "label": "Description" + }, + { + "default": "0", + "depends_on": "eval:doc.is_exchange_gain_loss", + "fieldname": "is_exchange_gain_loss", + "fieldtype": "Check", + "label": "Is Exchange Gain / Loss?", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-03-06 07:11:57.739619", + "modified": "2024-11-05 16:07:47.307971", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry Deduction", diff --git a/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.py b/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.py index fc67c526b28b..ae4134fc27a3 100644 --- a/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.py +++ b/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.py @@ -18,6 +18,7 @@ class PaymentEntryDeduction(Document): amount: DF.Currency cost_center: DF.Link description: DF.SmallText | None + is_exchange_gain_loss: DF.Check parent: DF.Data parentfield: DF.Data parenttype: DF.Data diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json index 23ed8252333f..361f516b8300 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json @@ -10,6 +10,7 @@ "due_date", "bill_no", "payment_term", + "payment_term_outstanding", "account_type", "payment_type", "column_break_4", @@ -18,7 +19,9 @@ "allocated_amount", "exchange_rate", "exchange_gain_loss", - "account" + "account", + "payment_request", + "payment_request_outstanding" ], "fields": [ { @@ -120,12 +123,33 @@ "fieldname": "payment_type", "fieldtype": "Data", "label": "Payment Type" + }, + { + "fieldname": "payment_request", + "fieldtype": "Link", + "label": "Payment Request", + "options": "Payment Request" + }, + { + "depends_on": "eval: doc.payment_term", + "fieldname": "payment_term_outstanding", + "fieldtype": "Float", + "label": "Payment Term Outstanding", + "read_only": 1 + }, + { + "depends_on": "eval: doc.payment_request && doc.payment_request_outstanding", + "fieldname": "payment_request_outstanding", + "fieldtype": "Float", + "is_virtual": 1, + "label": "Payment Request Outstanding", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-04-05 09:44:08.310593", + "modified": "2024-09-16 18:11:50.019343", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry Reference", diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py index 4a027b4ee32b..2ac92ba4a841 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py @@ -1,7 +1,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt - +import frappe from frappe.model.document import Document @@ -25,11 +25,19 @@ class PaymentEntryReference(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data + payment_request: DF.Link | None + payment_request_outstanding: DF.Float payment_term: DF.Link | None + payment_term_outstanding: DF.Float payment_type: DF.Data | None reference_doctype: DF.Link reference_name: DF.DynamicLink total_amount: DF.Float # end: auto-generated types - pass + @property + def payment_request_outstanding(self): + if not self.payment_request: + return + + return frappe.db.get_value("Payment Request", self.payment_request, "outstanding_amount") diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 8481fd8ffa4f..db4a4b0f268a 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -153,10 +153,7 @@ def get_nonreconciled_payment_entries(self): self.add_payment_entries(non_reconciled_payments) def get_payment_entries(self): - if self.default_advance_account: - party_account = [self.receivable_payable_account, self.default_advance_account] - else: - party_account = [self.receivable_payable_account] + party_account = [self.receivable_payable_account] order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order" condition = frappe._dict( @@ -187,6 +184,7 @@ def get_payment_entries(self): self.party, party_account, order_doctype, + default_advance_account=self.default_advance_account, against_all_orders=True, limit=self.payment_limit, condition=condition, @@ -211,12 +209,14 @@ def get_jv_entries(self): if self.get("cost_center"): conditions.append(jea.cost_center == self.cost_center) - dr_or_cr = ( - "credit_in_account_currency" - if erpnext.get_party_account_type(self.party_type) == "Receivable" - else "debit_in_account_currency" - ) - conditions.append(jea[dr_or_cr].gt(0)) + account_type = erpnext.get_party_account_type(self.party_type) + + if account_type == "Receivable": + dr_or_cr = jea.credit_in_account_currency - jea.debit_in_account_currency + elif account_type == "Payable": + dr_or_cr = jea.debit_in_account_currency - jea.credit_in_account_currency + + conditions.append(dr_or_cr.gt(0)) if self.bank_cash_account: conditions.append(jea.against_account.like(f"%%{self.bank_cash_account}%%")) @@ -231,7 +231,7 @@ def get_jv_entries(self): je.posting_date, je.remark.as_("remarks"), jea.name.as_("reference_row"), - jea[dr_or_cr].as_("amount"), + dr_or_cr.as_("amount"), jea.is_advance, jea.exchange_rate, jea.account_currency.as_("currency"), @@ -323,6 +323,7 @@ def get_dr_or_cr_notes(self): "posting_date": inv.posting_date, "currency": inv.currency, "cost_center": inv.cost_center, + "remarks": inv.remarks, } ) ) @@ -370,6 +371,10 @@ def get_invoice_entries(self): if self.invoice_limit: non_reconciled_invoices = non_reconciled_invoices[: self.invoice_limit] + non_reconciled_invoices = sorted( + non_reconciled_invoices, key=lambda k: k["posting_date"] or getdate(nowdate()) + ) + self.add_invoice_entries(non_reconciled_invoices) def add_invoice_entries(self, non_reconciled_invoices): diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 5aa411158a8f..3f0fb29d6711 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -5,7 +5,7 @@ import frappe from frappe import qb from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import add_days, flt, nowdate +from frappe.utils import add_days, add_years, flt, getdate, nowdate, today from erpnext import get_default_cost_center from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry @@ -13,6 +13,7 @@ from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.party import get_party_account +from erpnext.accounts.utils import get_fiscal_year from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.stock.doctype.item.test_item import create_item @@ -631,6 +632,42 @@ def test_journal_against_invoice(self): self.assertEqual(len(pr.get("invoices")), 0) self.assertEqual(len(pr.get("payments")), 0) + def test_negative_debit_or_credit_journal_against_invoice(self): + transaction_date = nowdate() + amount = 100 + si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date) + + # credit debtors account to record a payment + je = self.create_journal_entry(self.bank, self.debit_to, amount, transaction_date) + je.accounts[1].party_type = "Customer" + je.accounts[1].party = self.customer + je.accounts[1].credit_in_account_currency = 0 + je.accounts[1].debit_in_account_currency = -1 * amount + je.save() + je.submit() + + pr = self.create_payment_reconciliation() + + pr.get_unreconciled_entries() + invoices = [x.as_dict() for x in pr.get("invoices")] + payments = [x.as_dict() for x in pr.get("payments")] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + # Difference amount should not be calculated for base currency accounts + for row in pr.allocation: + self.assertEqual(flt(row.get("difference_amount")), 0.0) + + pr.reconcile() + + # assert outstanding + si.reload() + self.assertEqual(si.status, "Paid") + self.assertEqual(si.outstanding_amount, 0) + + # check PR tool output + self.assertEqual(len(pr.get("invoices")), 0) + self.assertEqual(len(pr.get("payments")), 0) + def test_journal_against_journal(self): transaction_date = nowdate() sales = "Sales - _PR" @@ -953,6 +990,100 @@ def test_difference_amount_via_journal_entry(self): frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss" ) + def test_difference_amount_via_negative_debit_or_credit_journal_entry(self): + # Make Sale Invoice + si = self.create_sales_invoice( + qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True + ) + si.customer = self.customer4 + si.currency = "EUR" + si.conversion_rate = 85 + si.debit_to = self.debtors_eur + si.save().submit() + + # Make payment using Journal Entry + je1 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 100, nowdate()) + je1.multi_currency = 1 + je1.accounts[0].exchange_rate = 1 + je1.accounts[0].credit_in_account_currency = -8000 + je1.accounts[0].credit = -8000 + je1.accounts[0].debit_in_account_currency = 0 + je1.accounts[0].debit = 0 + je1.accounts[1].party_type = "Customer" + je1.accounts[1].party = self.customer4 + je1.accounts[1].exchange_rate = 80 + je1.accounts[1].credit_in_account_currency = 100 + je1.accounts[1].credit = 8000 + je1.accounts[1].debit_in_account_currency = 0 + je1.accounts[1].debit = 0 + je1.save() + je1.submit() + + je2 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 200, nowdate()) + je2.multi_currency = 1 + je2.accounts[0].exchange_rate = 1 + je2.accounts[0].credit_in_account_currency = -16000 + je2.accounts[0].credit = -16000 + je2.accounts[0].debit_in_account_currency = 0 + je2.accounts[0].debit = 0 + je2.accounts[1].party_type = "Customer" + je2.accounts[1].party = self.customer4 + je2.accounts[1].exchange_rate = 80 + je2.accounts[1].credit_in_account_currency = 200 + je1.accounts[1].credit = 16000 + je1.accounts[1].debit_in_account_currency = 0 + je1.accounts[1].debit = 0 + je2.save() + je2.submit() + + pr = self.create_payment_reconciliation() + pr.party = self.customer4 + pr.receivable_payable_account = self.debtors_eur + pr.get_unreconciled_entries() + + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 2) + + # Test exact payment allocation + invoices = [x.as_dict() for x in pr.invoices] + payments = [pr.payments[0].as_dict()] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + self.assertEqual(pr.allocation[0].allocated_amount, 100) + self.assertEqual(pr.allocation[0].difference_amount, -500) + + # Test partial payment allocation (with excess payment entry) + pr.set("allocation", []) + pr.get_unreconciled_entries() + invoices = [x.as_dict() for x in pr.invoices] + payments = [pr.payments[1].as_dict()] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.allocation[0].difference_account = "Exchange Gain/Loss - _PR" + + self.assertEqual(pr.allocation[0].allocated_amount, 100) + self.assertEqual(pr.allocation[0].difference_amount, -500) + + # Check if difference journal entry gets generated for difference amount after reconciliation + pr.reconcile() + total_credit_amount = frappe.db.get_all( + "Journal Entry Account", + {"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name}, + "sum(credit) as amount", + group_by="reference_name", + )[0].amount + + # total credit includes the exchange gain/loss amount + self.assertEqual(flt(total_credit_amount, 2), 8500) + + jea_parent = frappe.db.get_all( + "Journal Entry Account", + filters={"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name, "credit": 500}, + fields=["parent"], + )[0] + self.assertEqual( + frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss" + ) + def test_difference_amount_via_payment_entry(self): # Make Sale Invoice si = self.create_sales_invoice( @@ -1845,6 +1976,78 @@ def test_cr_note_payment_limit_filter(self): self.assertEqual(len(pr.invoices), 1) self.assertEqual(len(pr.payments), 1) + def test_reconciliation_on_closed_period_payment(self): + # create backdated fiscal year + first_fy_start_date = frappe.db.get_value("Fiscal Year", {"disabled": 0}, "min(year_start_date)") + prev_fy_start_date = add_years(first_fy_start_date, -1) + prev_fy_end_date = add_days(first_fy_start_date, -1) + create_fiscal_year( + company=self.company, year_start_date=prev_fy_start_date, year_end_date=prev_fy_end_date + ) + + # make journal entry for previous year + je_1 = frappe.new_doc("Journal Entry") + je_1.posting_date = add_days(prev_fy_start_date, 20) + je_1.company = self.company + je_1.user_remark = "test" + je_1.set( + "accounts", + [ + { + "account": self.debit_to, + "cost_center": self.cost_center, + "party_type": "Customer", + "party": self.customer, + "debit_in_account_currency": 0, + "credit_in_account_currency": 1000, + }, + { + "account": self.bank, + "cost_center": self.sub_cc.name, + "credit_in_account_currency": 0, + "debit_in_account_currency": 500, + }, + { + "account": self.cash, + "cost_center": self.sub_cc.name, + "credit_in_account_currency": 0, + "debit_in_account_currency": 500, + }, + ], + ) + je_1.submit() + + # make period closing voucher + pcv = make_period_closing_voucher( + company=self.company, cost_center=self.cost_center, posting_date=prev_fy_end_date + ) + pcv.reload() + # check if period closing voucher is completed + self.assertEqual(pcv.gle_processing_status, "Completed") + + # make journal entry for active year + je_2 = self.create_journal_entry( + acc1=self.debit_to, acc2=self.income_account, amount=1000, posting_date=today() + ) + je_2.accounts[0].party_type = "Customer" + je_2.accounts[0].party = self.customer + je_2.submit() + + # process reconciliation on closed period payment + pr = self.create_payment_reconciliation(party_is_customer=True) + pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = None + pr.get_unreconciled_entries() + invoices = [invoice.as_dict() for invoice in pr.invoices] + payments = [payment.as_dict() for payment in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + je_1.reload() + je_2.reload() + + # check whether the payment reconciliation is done on the closed period + self.assertEqual(pr.get("invoices"), []) + self.assertEqual(pr.get("payments"), []) + def make_customer(customer_name, currency=None): if not frappe.db.exists("Customer", customer_name): @@ -1872,3 +2075,63 @@ def make_supplier(supplier_name, currency=None): return supplier.name else: return supplier_name + + +def create_fiscal_year(company, year_start_date, year_end_date): + fy_docname = frappe.db.exists( + "Fiscal Year", {"year_start_date": year_start_date, "year_end_date": year_end_date} + ) + if not fy_docname: + fy_doc = frappe.get_doc( + { + "doctype": "Fiscal Year", + "year": f"{getdate(year_start_date).year}-{getdate(year_end_date).year}", + "year_start_date": year_start_date, + "year_end_date": year_end_date, + "companies": [{"company": company}], + } + ).save() + return fy_doc + else: + fy_doc = frappe.get_doc("Fiscal Year", fy_docname) + if not frappe.db.exists("Fiscal Year Company", {"parent": fy_docname, "company": company}): + fy_doc.append("companies", {"company": company}) + fy_doc.save() + return fy_doc + + +def make_period_closing_voucher(company, cost_center, posting_date=None, submit=True): + from erpnext.accounts.doctype.account.test_account import create_account + + parent_account = frappe.db.get_value( + "Account", {"company": company, "account_name": "Current Liabilities", "is_group": 1}, "name" + ) + surplus_account = create_account( + account_name="Reserve and Surplus", + is_group=0, + company=company, + root_type="Liability", + report_type="Balance Sheet", + account_currency="INR", + parent_account=parent_account, + doctype="Account", + ) + fy = get_fiscal_year(posting_date, company=company) + pcv = frappe.get_doc( + { + "doctype": "Period Closing Voucher", + "transaction_date": posting_date or today(), + "period_start_date": fy[1], + "period_end_date": fy[2], + "company": company, + "fiscal_year": fy[0], + "cost_center": cost_center, + "closing_account_head": surplus_account, + "remarks": "test", + } + ) + pcv.insert() + if submit: + pcv.submit() + + return pcv diff --git a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json index d199236ae996..010e93558cfd 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json +++ b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json @@ -14,7 +14,7 @@ "amount", "difference_amount", "sec_break1", - "remark", + "remarks", "currency", "exchange_rate", "cost_center" @@ -74,12 +74,6 @@ "fieldname": "sec_break1", "fieldtype": "Section Break" }, - { - "fieldname": "remark", - "fieldtype": "Small Text", - "label": "Remark", - "read_only": 1 - }, { "fieldname": "currency", "fieldtype": "Link", @@ -105,12 +99,18 @@ "fieldtype": "Link", "label": "Cost Center", "options": "Cost Center" + }, + { + "fieldname": "remarks", + "fieldtype": "Small Text", + "label": "Remarks", + "read_only": 1 } ], "is_virtual": 1, "istable": 1, "links": [], - "modified": "2023-11-17 17:33:34.818530", + "modified": "2024-10-29 16:24:43.021230", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Payment", diff --git a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.py b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.py index 4ab80ecaafe4..49c17eae41b7 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.py +++ b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.py @@ -27,7 +27,7 @@ class PaymentReconciliationPayment(Document): reference_name: DF.DynamicLink | None reference_row: DF.Data | None reference_type: DF.Link | None - remark: DF.SmallText | None + remarks: DF.SmallText | None # end: auto-generated types @staticmethod diff --git a/erpnext/accounts/doctype/payment_request/payment_request.js b/erpnext/accounts/doctype/payment_request/payment_request.js index e45aa512fe80..50f96a4e2b6b 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.js +++ b/erpnext/accounts/doctype/payment_request/payment_request.js @@ -48,8 +48,8 @@ frappe.ui.form.on("Payment Request", "refresh", function (frm) { } if ( - (!frm.doc.payment_gateway_account || frm.doc.payment_request_type == "Outward") && - frm.doc.status == "Initiated" + frm.doc.payment_request_type == "Outward" && + ["Initiated", "Partially Paid"].includes(frm.doc.status) ) { frm.add_custom_button(__("Create Payment Entry"), function () { frappe.call({ diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json index 0537ee6d3a38..2eef429cd3aa 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.json +++ b/erpnext/accounts/doctype/payment_request/payment_request.json @@ -9,18 +9,22 @@ "transaction_date", "column_break_2", "naming_series", + "company", "mode_of_payment", "party_details", "party_type", "party", + "party_name", "column_break_4", "reference_doctype", "reference_name", "transaction_details", "grand_total", + "currency", "is_a_subscription", "column_break_18", - "currency", + "outstanding_amount", + "party_account_currency", "subscription_section", "subscription_plans", "bank_account_details", @@ -68,6 +72,7 @@ { "fieldname": "transaction_date", "fieldtype": "Date", + "in_preview": 1, "label": "Transaction Date" }, { @@ -132,7 +137,8 @@ "no_copy": 1, "options": "reference_doctype", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "transaction_details", @@ -140,12 +146,14 @@ "label": "Transaction Details" }, { - "description": "Amount in customer's currency", + "description": "Amount in transaction currency", "fieldname": "grand_total", "fieldtype": "Currency", + "in_preview": 1, "label": "Amount", "non_negative": 1, - "options": "currency" + "options": "currency", + "reqd": 1 }, { "default": "0", @@ -390,13 +398,44 @@ "options": "Payment Request", "print_hide": 1, "read_only": 1 + }, + { + "depends_on": "eval: doc.docstatus === 1", + "description": "Amount in party's bank account currency", + "fieldname": "outstanding_amount", + "fieldtype": "Currency", + "in_preview": 1, + "label": "Outstanding Amount", + "non_negative": 1, + "options": "party_account_currency", + "read_only": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "read_only": 1 + }, + { + "fieldname": "party_account_currency", + "fieldtype": "Link", + "label": "Party Account Currency", + "options": "Currency", + "read_only": 1 + }, + { + "fieldname": "party_name", + "fieldtype": "Data", + "label": "Party Name", + "read_only": 1 } ], "in_create": 1, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-06-20 13:54:55.245774", + "modified": "2024-10-23 12:23:40.117336", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Request", @@ -431,7 +470,8 @@ "write": 1 } ], + "show_preview_popup": 1, "sort_field": "modified", "sort_order": "DESC", "states": [] -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index a2c6a9d856a1..61bb2932d2b4 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -1,11 +1,13 @@ import json import frappe -from frappe import _ +from frappe import _, qb from frappe.model.document import Document +from frappe.query_builder.functions import Abs, Sum from frappe.utils import flt, nowdate from frappe.utils.background_jobs import enqueue +from erpnext import get_company_currency from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, ) @@ -15,9 +17,18 @@ ) from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate from erpnext.accounts.party import get_party_account, get_party_bank_account -from erpnext.accounts.utils import get_account_currency +from erpnext.accounts.utils import get_account_currency, get_currency_precision from erpnext.utilities import payment_app_import_guard +ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST = [ + "Sales Order", + "Purchase Order", + "Sales Invoice", + "Purchase Invoice", + "POS Invoice", + "Fees", +] + def _get_payment_gateway_controller(*args, **kwargs): with payment_app_import_guard(): @@ -45,6 +56,7 @@ class PaymentRequest(Document): bank_account: DF.Link | None bank_account_no: DF.ReadOnly | None branch_code: DF.ReadOnly | None + company: DF.Link | None cost_center: DF.Link | None currency: DF.Link | None email_to: DF.Data | None @@ -56,16 +68,19 @@ class PaymentRequest(Document): mode_of_payment: DF.Link | None mute_email: DF.Check naming_series: DF.Literal["ACC-PRQ-.YYYY.-"] + outstanding_amount: DF.Currency party: DF.DynamicLink | None + party_account_currency: DF.Link | None + party_name: DF.Data | None party_type: DF.Link | None payment_account: DF.ReadOnly | None - payment_channel: DF.Literal["", "Email", "Phone"] + payment_channel: DF.Literal["", "Email", "Phone", "Other"] payment_gateway: DF.ReadOnly | None payment_gateway_account: DF.Link | None payment_order: DF.Link | None payment_request_type: DF.Literal["Outward", "Inward"] payment_url: DF.Data | None - print_format: DF.Literal + print_format: DF.Literal[None] project: DF.Link | None reference_doctype: DF.Link | None reference_name: DF.DynamicLink | None @@ -99,6 +114,12 @@ def validate_reference_document(self): frappe.throw(_("To create a Payment Request reference document is required")) def validate_payment_request_amount(self): + if self.grand_total == 0: + frappe.throw( + _("{0} cannot be zero").format(self.get_label_from_fieldname("grand_total")), + title=_("Invalid Amount"), + ) + existing_payment_request_amount = flt( get_existing_payment_request_amount(self.reference_doctype, self.reference_name) ) @@ -146,6 +167,28 @@ def validate_subscription_details(self): ).format(self.grand_total, amount) ) + def before_submit(self): + if ( + self.currency != self.party_account_currency + and self.party_account_currency == get_company_currency(self.company) + ): + # set outstanding amount in party account currency + invoice = frappe.get_value( + self.reference_doctype, + self.reference_name, + ["rounded_total", "grand_total", "base_rounded_total", "base_grand_total"], + as_dict=1, + ) + grand_total = invoice.get("rounded_total") or invoice.get("grand_total") + base_grand_total = invoice.get("base_rounded_total") or invoice.get("base_grand_total") + self.outstanding_amount = flt( + self.grand_total / grand_total * base_grand_total, + self.precision("outstanding_amount"), + ) + + else: + self.outstanding_amount = self.grand_total + def on_submit(self): if self.payment_request_type == "Outward": self.db_set("status", "Initiated") @@ -261,12 +304,12 @@ def get_payment_url(self): return controller.get_payment_url( **{ "amount": flt(self.grand_total, self.precision("grand_total")), - "title": data.company.encode("utf-8"), - "description": self.subject.encode("utf-8"), + "title": data.company, + "description": self.subject, "reference_doctype": "Payment Request", "reference_docname": self.name, "payer_email": self.email_to or frappe.session.user, - "payer_name": frappe.safe_encode(data.customer_name), + "payer_name": data.customer_name, "order_id": self.name, "currency": self.currency, } @@ -274,7 +317,7 @@ def get_payment_url(self): def set_as_paid(self): if self.payment_channel == "Phone": - self.db_set("status", "Paid") + self.db_set({"status": "Paid", "outstanding_amount": 0}) else: payment_entry = self.create_payment_entry() @@ -295,26 +338,32 @@ def create_payment_entry(self, submit=True): else: party_account = get_party_account("Customer", ref_doc.get("customer"), ref_doc.company) - party_account_currency = ref_doc.get("party_account_currency") or get_account_currency(party_account) + party_account_currency = ( + self.get("party_account_currency") + or ref_doc.get("party_account_currency") + or get_account_currency(party_account) + ) + + party_amount = bank_amount = self.outstanding_amount - bank_amount = self.grand_total if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency: - party_amount = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total") - else: - party_amount = self.grand_total + exchange_rate = ref_doc.get("conversion_rate") + bank_amount = flt(self.outstanding_amount / exchange_rate, self.precision("grand_total")) + # outstanding amount is already in Part's account currency payment_entry = get_payment_entry( self.reference_doctype, self.reference_name, party_amount=party_amount, bank_account=self.payment_account, bank_amount=bank_amount, + created_from_payment_request=True, ) payment_entry.update( { "mode_of_payment": self.mode_of_payment, - "reference_no": self.name, + "reference_no": self.name, # to prevent validation error "reference_date": nowdate(), "remarks": "Payment Entry against {} {} via Payment Request {}".format( self.reference_doctype, self.reference_name, self.name @@ -322,6 +371,9 @@ def create_payment_entry(self, submit=True): } ) + # Allocate payment_request for each reference in payment_entry (Payment Term can splits the row) + self._allocate_payment_request_to_pe_references(references=payment_entry.references) + # Update dimensions payment_entry.update( { @@ -330,14 +382,6 @@ def create_payment_entry(self, submit=True): } ) - if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency: - amount = payment_entry.base_paid_amount - else: - amount = self.grand_total - - payment_entry.received_amount = amount - payment_entry.get("references")[0].allocated_amount = amount - # Update 'Paid Amount' on Forex transactions if self.currency != ref_doc.company_currency: if ( @@ -428,6 +472,62 @@ def create_subscription(self, payment_provider, gateway_controller, data): return create_stripe_subscription(gateway_controller, data) + def _allocate_payment_request_to_pe_references(self, references): + """ + Allocate the Payment Request to the Payment Entry references based on\n + - Allocated Amount. + - Outstanding Amount of Payment Request.\n + Payment Request is doc itself and references are the rows of Payment Entry. + """ + if len(references) == 1: + references[0].payment_request = self.name + return + + precision = references[0].precision("allocated_amount") + outstanding_amount = self.outstanding_amount + + # to manage rows + row_number = 1 + MOVE_TO_NEXT_ROW = 1 + TO_SKIP_NEW_ROW = 2 + NEW_ROW_ADDED = False + + while row_number <= len(references): + row = references[row_number - 1] + + # update the idx to maintain the order + row.idx = row_number + + if outstanding_amount == 0: + if not NEW_ROW_ADDED: + break + + row_number += MOVE_TO_NEXT_ROW + continue + + # allocate the payment request to the row + row.payment_request = self.name + + if row.allocated_amount <= outstanding_amount: + outstanding_amount = flt(outstanding_amount - row.allocated_amount, precision) + row_number += MOVE_TO_NEXT_ROW + else: + remaining_allocated_amount = flt(row.allocated_amount - outstanding_amount, precision) + row.allocated_amount = outstanding_amount + outstanding_amount = 0 + + # create a new row without PR for remaining unallocated amount + new_row = frappe.copy_doc(row) + references.insert(row_number, new_row) + + # update new row + new_row.idx = row_number + 1 + new_row.payment_request = None + new_row.allocated_amount = remaining_allocated_amount + + NEW_ROW_ADDED = True + row_number += TO_SKIP_NEW_ROW + @frappe.whitelist(allow_guest=True) def make_payment_request(**args): @@ -435,6 +535,9 @@ def make_payment_request(**args): args = frappe._dict(args) + if args.dt not in ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST: + frappe.throw(_("Payment Requests cannot be created against: {0}").format(frappe.bold(args.dt))) + ref_doc = frappe.get_doc(args.dt, args.dn) gateway_account = get_gateway_details(args) or frappe._dict() @@ -458,10 +561,38 @@ def make_payment_request(**args): {"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": 0}, ) - existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn) + # fetches existing payment request `grand_total` amount + existing_payment_request_amount = get_existing_payment_request_amount(ref_doc.doctype, ref_doc.name) - if existing_payment_request_amount: + existing_paid_amount = get_existing_paid_amount(ref_doc.doctype, ref_doc.name) + + def validate_and_calculate_grand_total(grand_total, existing_payment_request_amount): grand_total -= existing_payment_request_amount + if not grand_total: + frappe.throw(_("Payment Request is already created")) + return grand_total + + if existing_payment_request_amount: + if args.order_type == "Shopping Cart": + # If Payment Request is in an advanced stage, then create for remaining amount. + if get_existing_payment_request_amount( + ref_doc.doctype, ref_doc.name, ["Initiated", "Partially Paid", "Payment Ordered", "Paid"] + ): + grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount) + else: + # If PR's are processed, cancel all of them. + cancel_old_payment_requests(ref_doc.doctype, ref_doc.name) + else: + grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount) + + if existing_paid_amount: + if ref_doc.party_account_currency == ref_doc.currency: + if ref_doc.conversion_rate: + grand_total -= flt(existing_paid_amount / ref_doc.conversion_rate) + else: + grand_total -= flt(existing_paid_amount) + else: + grand_total -= flt(existing_paid_amount / ref_doc.conversion_rate) if draft_payment_request: frappe.db.set_value( @@ -476,6 +607,13 @@ def make_payment_request(**args): "Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward" ) + party_type = args.get("party_type") or "Customer" + party_account_currency = ref_doc.get("party_account_currency") + + if not party_account_currency: + party_account = get_party_account(party_type, ref_doc.get(party_type.lower()), ref_doc.company) + party_account_currency = get_account_currency(party_account) + pr.update( { "payment_gateway_account": gateway_account.get("name"), @@ -484,6 +622,7 @@ def make_payment_request(**args): "payment_channel": gateway_account.get("payment_channel"), "payment_request_type": args.get("payment_request_type"), "currency": ref_doc.currency, + "party_account_currency": party_account_currency, "grand_total": grand_total, "mode_of_payment": args.mode_of_payment, "email_to": args.recipient_id or ref_doc.owner, @@ -491,9 +630,11 @@ def make_payment_request(**args): "message": gateway_account.get("message") or get_dummy_message(ref_doc), "reference_doctype": args.dt, "reference_name": args.dn, - "party_type": args.get("party_type") or "Customer", + "company": ref_doc.get("company"), + "party_type": party_type, "party": args.get("party") or ref_doc.get("customer"), "bank_account": bank_account, + "party_name": args.get("party_name") or ref_doc.get("customer_name"), } ) @@ -514,6 +655,8 @@ def make_payment_request(**args): if frappe.db.get_single_value("Accounts Settings", "create_pr_in_draft_status", cache=True): pr.insert(ignore_permissions=True) if args.submit_doc: + if pr.get("__unsaved"): + pr.insert(ignore_permissions=True) pr.submit() if args.order_type == "Shopping Cart": @@ -535,9 +678,11 @@ def get_amount(ref_doc, payment_account=None): elif dt in ["Sales Invoice", "Purchase Invoice"]: if not ref_doc.get("is_pos"): if ref_doc.party_account_currency == ref_doc.currency: - grand_total = flt(ref_doc.grand_total) + grand_total = flt(ref_doc.rounded_total or ref_doc.grand_total) else: - grand_total = flt(ref_doc.base_grand_total) / ref_doc.conversion_rate + grand_total = flt( + flt(ref_doc.base_rounded_total or ref_doc.base_grand_total) / ref_doc.conversion_rate + ) elif dt == "Sales Invoice": for pay in ref_doc.payments: if pay.type == "Phone" and pay.account == payment_account: @@ -552,31 +697,94 @@ def get_amount(ref_doc, payment_account=None): grand_total = ref_doc.outstanding_amount if grand_total > 0: - return grand_total + return flt(grand_total, get_currency_precision()) else: frappe.throw(_("Payment Entry is already created")) -def get_existing_payment_request_amount(ref_dt, ref_dn): +def get_irequest_status(payment_requests: None | list = None) -> list: + IR = frappe.qb.DocType("Integration Request") + res = [] + if payment_requests: + res = ( + frappe.qb.from_(IR) + .select(IR.name) + .where(IR.reference_doctype.eq("Payment Request")) + .where(IR.reference_docname.isin(payment_requests)) + .where(IR.status.isin(["Authorized", "Completed"])) + .run(as_dict=True) + ) + return res + + +def cancel_old_payment_requests(ref_dt, ref_dn): + PR = frappe.qb.DocType("Payment Request") + + if res := ( + frappe.qb.from_(PR) + .select(PR.name) + .where(PR.reference_doctype == ref_dt) + .where(PR.reference_name == ref_dn) + .where(PR.docstatus == 1) + .where(PR.status.isin(["Draft", "Requested"])) + .run(as_dict=True) + ): + if get_irequest_status([x.name for x in res]): + frappe.throw(_("Another Payment Request is already processed")) + else: + for x in res: + doc = frappe.get_doc("Payment Request", x.name) + doc.flags.ignore_permissions = True + doc.cancel() + + if ireqs := get_irequests_of_payment_request(doc.name): + for ireq in ireqs: + frappe.db.set_value("Integration Request", ireq.name, "status", "Cancelled") + + +def get_existing_payment_request_amount(ref_dt, ref_dn, statuses: list | None = None) -> list: """ - Get the existing payment request which are unpaid or partially paid for payment channel other than Phone - and get the summation of existing paid payment request for Phone payment channel. + Return the total amount of Payment Requests against a reference document. """ - existing_payment_request_amount = frappe.db.sql( - """ - select sum(grand_total) - from `tabPayment Request` - where - reference_doctype = %s - and reference_name = %s - and docstatus = 1 - and (status != 'Paid' - or (payment_channel = 'Phone' - and status = 'Paid')) - """, - (ref_dt, ref_dn), + PR = frappe.qb.DocType("Payment Request") + + query = ( + frappe.qb.from_(PR) + .select(Sum(PR.grand_total)) + .where(PR.reference_doctype == ref_dt) + .where(PR.reference_name == ref_dn) + .where(PR.docstatus == 1) + ) + + if statuses: + query = query.where(PR.status.isin(statuses)) + + response = query.run() + + return response[0][0] if response[0] else 0 + + +def get_existing_paid_amount(doctype, name): + PL = frappe.qb.DocType("Payment Ledger Entry") + PER = frappe.qb.DocType("Payment Entry Reference") + + query = ( + frappe.qb.from_(PL) + .left_join(PER) + .on( + (PER.reference_doctype == PL.against_voucher_type) & (PER.reference_name == PL.against_voucher_no) + ) + .select(Abs(Sum(PL.amount)).as_("total_paid_amount")) + .where(PL.against_voucher_type.eq(doctype)) + .where(PL.against_voucher_no.eq(name)) + .where(PL.amount < 0) + .where(PL.delinked == 0) + .where(PER.docstatus == 1) + .where(PER.payment_request.isnull()) ) - return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0 + response = query.run() + + return response[0][0] if response[0] else 0 def get_gateway_details(args): # nosemgrep @@ -623,41 +831,66 @@ def make_payment_entry(docname): return doc.create_payment_entry(submit=False).as_dict() -def update_payment_req_status(doc, method): - from erpnext.accounts.doctype.payment_entry.payment_entry import get_reference_details +def update_payment_requests_as_per_pe_references(references=None, cancel=False): + """ + Update Payment Request's `Status` and `Outstanding Amount` based on Payment Entry Reference's `Allocated Amount`. + """ + if not references: + return - for ref in doc.references: - payment_request_name = frappe.db.get_value( - "Payment Request", - { - "reference_doctype": ref.reference_doctype, - "reference_name": ref.reference_name, - "docstatus": 1, - }, + precision = references[0].precision("allocated_amount") + + referenced_payment_requests = frappe.get_all( + "Payment Request", + filters={"name": ["in", {row.payment_request for row in references if row.payment_request}]}, + fields=[ + "name", + "grand_total", + "outstanding_amount", + "payment_request_type", + ], + ) + + referenced_payment_requests = {pr.name: pr for pr in referenced_payment_requests} + + for ref in references: + if not ref.payment_request: + continue + + payment_request = referenced_payment_requests[ref.payment_request] + pr_outstanding = payment_request["outstanding_amount"] + + # update outstanding amount + new_outstanding_amount = flt( + pr_outstanding + ref.allocated_amount if cancel else pr_outstanding - ref.allocated_amount, + precision, ) - if payment_request_name: - ref_details = get_reference_details( - ref.reference_doctype, - ref.reference_name, - doc.party_account_currency, - doc.party_type, - doc.party, + # to handle same payment request for the multiple allocations + payment_request["outstanding_amount"] = new_outstanding_amount + + if not cancel and new_outstanding_amount < 0: + frappe.throw( + msg=_( + "The allocated amount is greater than the outstanding amount of Payment Request {0}" + ).format(ref.payment_request), + title=_("Invalid Allocated Amount"), ) - pay_req_doc = frappe.get_doc("Payment Request", payment_request_name) - status = pay_req_doc.status - if status != "Paid" and not ref_details.outstanding_amount: - status = "Paid" - elif status != "Partially Paid" and ref_details.outstanding_amount != ref_details.total_amount: - status = "Partially Paid" - elif ref_details.outstanding_amount == ref_details.total_amount: - if pay_req_doc.payment_request_type == "Outward": - status = "Initiated" - elif pay_req_doc.payment_request_type == "Inward": - status = "Requested" + # update status + if new_outstanding_amount == payment_request["grand_total"]: + status = "Initiated" if payment_request["payment_request_type"] == "Outward" else "Requested" + elif new_outstanding_amount == 0: + status = "Paid" + elif new_outstanding_amount > 0: + status = "Partially Paid" - pay_req_doc.db_set("status", status) + # update database + frappe.db.set_value( + "Payment Request", + ref.payment_request, + {"outstanding_amount": new_outstanding_amount, "status": status}, + ) def get_dummy_message(doc): @@ -741,3 +974,45 @@ def validate_payment(doc, method=None): doc.reference_docname ) ) + + +@frappe.whitelist() +def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len, filters): + # permission checks in `get_list()` + filters = frappe._dict(filters) + + if not filters.reference_doctype or not filters.reference_name: + return [] + + if txt: + filters.name = ["like", f"%{txt}%"] + + open_payment_requests = frappe.get_list( + "Payment Request", + filters=filters, + fields=["name", "grand_total", "outstanding_amount"], + order_by="transaction_date ASC,creation ASC", + ) + + return [ + ( + pr.name, + _("Grand Total: {0}").format(pr.grand_total), + _("Outstanding Amount: {0}").format(pr.outstanding_amount), + ) + for pr in open_payment_requests + ] + + +def get_irequests_of_payment_request(doc: str | None = None) -> list: + res = [] + if doc: + res = frappe.db.get_all( + "Integration Request", + { + "reference_doctype": "Payment Request", + "reference_docname": doc, + "status": "Queued", + }, + ) + return res diff --git a/erpnext/accounts/doctype/payment_request/payment_request_dashboard.py b/erpnext/accounts/doctype/payment_request/payment_request_dashboard.py new file mode 100644 index 000000000000..02ad5684792b --- /dev/null +++ b/erpnext/accounts/doctype/payment_request/payment_request_dashboard.py @@ -0,0 +1,14 @@ +from frappe import _ + + +def get_data(): + return { + "fieldname": "payment_request", + "internal_links": { + "Payment Entry": ["references", "payment_request"], + "Payment Order": ["references", "payment_order"], + }, + "transactions": [ + {"label": _("Payment"), "items": ["Payment Entry", "Payment Order"]}, + ], + } diff --git a/erpnext/accounts/doctype/payment_request/payment_request_list.js b/erpnext/accounts/doctype/payment_request/payment_request_list.js index 183ca7c45849..6e4aada66c61 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request_list.js +++ b/erpnext/accounts/doctype/payment_request/payment_request_list.js @@ -1,19 +1,18 @@ +const INDICATORS = { + "Partially Paid": "orange", + Cancelled: "red", + Draft: "red", + Failed: "red", + Initiated: "green", + Paid: "blue", + Requested: "green", +}; + frappe.listview_settings["Payment Request"] = { add_fields: ["status"], get_indicator: function (doc) { - if (doc.status == "Draft") { - return [__("Draft"), "gray", "status,=,Draft"]; - } - if (doc.status == "Requested") { - return [__("Requested"), "green", "status,=,Requested"]; - } else if (doc.status == "Initiated") { - return [__("Initiated"), "green", "status,=,Initiated"]; - } else if (doc.status == "Partially Paid") { - return [__("Partially Paid"), "orange", "status,=,Partially Paid"]; - } else if (doc.status == "Paid") { - return [__("Paid"), "blue", "status,=,Paid"]; - } else if (doc.status == "Cancelled") { - return [__("Cancelled"), "red", "status,=,Cancelled"]; - } + if (!doc.status || !INDICATORS[doc.status]) return; + + return [__(doc.status), INDICATORS[doc.status], `status,=,${doc.status}`]; }, }; diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index 6d15f84d7cf3..eadb714baa30 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -1,11 +1,14 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt +import re import unittest import frappe -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice @@ -15,6 +18,7 @@ test_dependencies = ["Currency Exchange", "Journal Entry", "Contact", "Address"] + payment_gateway = {"doctype": "Payment Gateway", "gateway": "_Test Gateway"} payment_method = [ @@ -278,3 +282,291 @@ def test_conversion_on_foreign_currency_accounts(self): self.assertEqual(pe.paid_amount, 800) self.assertEqual(pe.base_received_amount, 800) self.assertEqual(pe.received_amount, 10) + + def test_multiple_payment_if_partially_paid_for_same_currency(self): + so = make_sales_order(currency="INR", qty=1, rate=1000) + + pr = make_payment_request( + dt="Sales Order", + dn=so.name, + mute_email=1, + submit_doc=1, + return_doc=1, + ) + + self.assertEqual(pr.grand_total, 1000) + self.assertEqual(pr.outstanding_amount, pr.grand_total) + self.assertEqual(pr.party_account_currency, pr.currency) # INR + + so.load_from_db() + + # to make partial payment + pe = pr.create_payment_entry(submit=False) + pe.paid_amount = 200 + pe.references[0].allocated_amount = 200 + pe.submit() + + self.assertEqual(pe.references[0].payment_request, pr.name) + + so.load_from_db() + + pr.load_from_db() + self.assertEqual(pr.status, "Partially Paid") + self.assertEqual(pr.outstanding_amount, 800) + self.assertEqual(pr.grand_total, 1000) + + # complete payment + pe = pr.create_payment_entry() + + self.assertEqual(pe.paid_amount, 800) # paid amount set from pr's outstanding amount + self.assertEqual(pe.references[0].allocated_amount, 800) + self.assertEqual(pe.references[0].outstanding_amount, 800) # for Orders it is not zero + self.assertEqual(pe.references[0].payment_request, pr.name) + + so.load_from_db() + + pr.load_from_db() + self.assertEqual(pr.status, "Paid") + self.assertEqual(pr.outstanding_amount, 0) + self.assertEqual(pr.grand_total, 1000) + + # creating a more payment Request must not allowed + self.assertRaisesRegex( + frappe.exceptions.ValidationError, + re.compile(r"Payment Request is already created"), + make_payment_request, + dt="Sales Order", + dn=so.name, + mute_email=1, + submit_doc=1, + return_doc=1, + ) + + @change_settings("Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1}) + def test_multiple_payment_if_partially_paid_for_multi_currency(self): + pi = make_purchase_invoice(currency="USD", conversion_rate=50, qty=1, rate=100, do_not_save=1) + pi.credit_to = "Creditors - _TC" + pi.submit() + + pr = make_payment_request( + dt="Purchase Invoice", + dn=pi.name, + mute_email=1, + submit_doc=1, + return_doc=1, + ) + + # 100 USD -> 5000 INR + self.assertEqual(pr.grand_total, 100) + self.assertEqual(pr.outstanding_amount, 5000) + self.assertEqual(pr.currency, "USD") + self.assertEqual(pr.party_account_currency, "INR") + self.assertEqual(pr.status, "Initiated") + + # to make partial payment + pe = pr.create_payment_entry(submit=False) + pe.paid_amount = 2000 + pe.references[0].allocated_amount = 2000 + pe.submit() + + self.assertEqual(pe.references[0].payment_request, pr.name) + + pr.load_from_db() + self.assertEqual(pr.status, "Partially Paid") + self.assertEqual(pr.outstanding_amount, 3000) + self.assertEqual(pr.grand_total, 100) + + # complete payment + pe = pr.create_payment_entry() + self.assertEqual(pe.paid_amount, 3000) # paid amount set from pr's outstanding amount + self.assertEqual(pe.references[0].allocated_amount, 3000) + self.assertEqual(pe.references[0].outstanding_amount, 0) # for Invoices it will zero + self.assertEqual(pe.references[0].payment_request, pr.name) + + pr.load_from_db() + self.assertEqual(pr.status, "Paid") + self.assertEqual(pr.outstanding_amount, 0) + self.assertEqual(pr.grand_total, 100) + + # creating a more payment Request must not allowed + self.assertRaisesRegex( + frappe.exceptions.ValidationError, + re.compile(r"Payment Request is already created"), + make_payment_request, + dt="Purchase Invoice", + dn=pi.name, + mute_email=1, + submit_doc=1, + return_doc=1, + ) + + def test_single_payment_with_payment_term_for_same_currency(self): + create_payment_terms_template() + + po = create_purchase_order(do_not_save=1, currency="INR", qty=1, rate=20000) + po.payment_terms_template = "Test Receivable Template" # 84.746 and 15.254 + po.save() + po.submit() + + pr = make_payment_request( + dt="Purchase Order", + dn=po.name, + mute_email=1, + submit_doc=1, + return_doc=1, + ) + + self.assertEqual(pr.grand_total, 20000) + self.assertEqual(pr.outstanding_amount, pr.grand_total) + self.assertEqual(pr.party_account_currency, pr.currency) # INR + self.assertEqual(pr.status, "Initiated") + + po.load_from_db() + + pe = pr.create_payment_entry() + + self.assertEqual(len(pe.references), 2) + self.assertEqual(pe.paid_amount, 20000) + + # check 1st payment term + self.assertEqual(pe.references[0].allocated_amount, 16949.2) + self.assertEqual(pe.references[0].payment_request, pr.name) + + # check 2nd payment term + self.assertEqual(pe.references[1].allocated_amount, 3050.8) + self.assertEqual(pe.references[1].payment_request, pr.name) + + po.load_from_db() + + pr.load_from_db() + self.assertEqual(pr.status, "Paid") + self.assertEqual(pr.outstanding_amount, 0) + self.assertEqual(pr.grand_total, 20000) + + @change_settings("Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1}) + def test_single_payment_with_payment_term_for_multi_currency(self): + create_payment_terms_template() + + si = create_sales_invoice( + do_not_save=1, currency="USD", debit_to="Debtors - _TC", qty=1, rate=200, conversion_rate=50 + ) + si.payment_terms_template = "Test Receivable Template" # 84.746 and 15.254 + si.save() + si.submit() + + pr = make_payment_request( + dt="Sales Invoice", + dn=si.name, + mute_email=1, + submit_doc=1, + return_doc=1, + ) + + # 200 USD -> 10000 INR + self.assertEqual(pr.grand_total, 200) + self.assertEqual(pr.outstanding_amount, 10000) + self.assertEqual(pr.currency, "USD") + self.assertEqual(pr.party_account_currency, "INR") + + pe = pr.create_payment_entry() + self.assertEqual(len(pe.references), 2) + self.assertEqual(pe.paid_amount, 10000) + + # check 1st payment term + # convert it via dollar and conversion_rate + self.assertEqual(pe.references[0].allocated_amount, 8474.5) # multi currency conversion + self.assertEqual(pe.references[0].payment_request, pr.name) + + # check 2nd payment term + self.assertEqual(pe.references[1].allocated_amount, 1525.5) # multi currency conversion + self.assertEqual(pe.references[1].payment_request, pr.name) + + pr.load_from_db() + self.assertEqual(pr.status, "Paid") + self.assertEqual(pr.outstanding_amount, 0) + self.assertEqual(pr.grand_total, 200) + + def test_payment_cancel_process(self): + so = make_sales_order(currency="INR", qty=1, rate=1000) + + pr = make_payment_request( + dt="Sales Order", + dn=so.name, + mute_email=1, + submit_doc=1, + return_doc=1, + ) + + self.assertEqual(pr.grand_total, 1000) + self.assertEqual(pr.outstanding_amount, pr.grand_total) + + so.load_from_db() + + pe = pr.create_payment_entry(submit=False) + pe.paid_amount = 800 + pe.references[0].allocated_amount = 800 + pe.submit() + + self.assertEqual(pe.references[0].payment_request, pr.name) + + so.load_from_db() + + pr.load_from_db() + self.assertEqual(pr.status, "Partially Paid") + self.assertEqual(pr.outstanding_amount, 200) + self.assertEqual(pr.grand_total, 1000) + + # cancelling PE + pe.cancel() + + pr.load_from_db() + self.assertEqual(pr.status, "Requested") + self.assertEqual(pr.outstanding_amount, 1000) + self.assertEqual(pr.grand_total, 1000) + + so.load_from_db() + + def test_partial_paid_invoice_with_payment_request(self): + si = create_sales_invoice(currency="INR", qty=1, rate=5000) + si.save() + si.submit() + + pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC") + pe.reference_no = "PAYEE0002" + pe.reference_date = frappe.utils.nowdate() + pe.paid_amount = 2500 + pe.references[0].allocated_amount = 2500 + pe.save() + pe.submit() + + si.load_from_db() + pr = make_payment_request(dt="Sales Invoice", dn=si.name, mute_email=1) + + self.assertEqual(pr.grand_total, si.outstanding_amount) + + +def test_partial_paid_invoice_with_submitted_payment_entry(self): + pi = make_purchase_invoice(currency="INR", qty=1, rate=5000) + pi.save() + pi.submit() + + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") + pe.reference_no = "PURINV0001" + pe.reference_date = frappe.utils.nowdate() + pe.paid_amount = 2500 + pe.references[0].allocated_amount = 2500 + pe.save() + pe.submit() + pe.cancel() + + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") + pe.reference_no = "PURINV0002" + pe.reference_date = frappe.utils.nowdate() + pe.paid_amount = 2500 + pe.references[0].allocated_amount = 2500 + pe.save() + pe.submit() + + pi.load_from_db() + pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1) + self.assertEqual(pr.grand_total, pi.outstanding_amount) diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js index 82d8cb37fe7f..095310c7e706 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.js @@ -19,6 +19,24 @@ frappe.ui.form.on("Period Closing Voucher", { }); }, + fiscal_year: function (frm) { + if (frm.doc.fiscal_year) { + frappe.call({ + method: "erpnext.accounts.doctype.period_closing_voucher.period_closing_voucher.get_period_start_end_date", + args: { + fiscal_year: frm.doc.fiscal_year, + company: frm.doc.company, + }, + callback: function (r) { + if (r.message) { + frm.set_value("period_start_date", r.message[0]); + frm.set_value("period_end_date", r.message[1]); + } + }, + }); + } + }, + refresh: function (frm) { if (frm.doc.docstatus > 0) { frm.add_custom_button( diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json index 624b5f82f64c..f41cff0e0d8f 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json @@ -6,39 +6,32 @@ "engine": "InnoDB", "field_order": [ "transaction_date", - "posting_date", + "company", "fiscal_year", - "year_start_date", + "period_start_date", + "period_end_date", "amended_from", - "company", "column_break1", "closing_account_head", - "remarks", "gle_processing_status", + "remarks", "error_message" ], "fields": [ { + "default": "Today", "fieldname": "transaction_date", "fieldtype": "Date", "label": "Transaction Date", "oldfieldname": "transaction_date", "oldfieldtype": "Date" }, - { - "fieldname": "posting_date", - "fieldtype": "Date", - "label": "Posting Date", - "oldfieldname": "posting_date", - "oldfieldtype": "Date", - "reqd": 1 - }, { "fieldname": "fiscal_year", "fieldtype": "Link", "in_list_view": 1, "in_standard_filter": 1, - "label": "Closing Fiscal Year", + "label": "Fiscal Year", "oldfieldname": "fiscal_year", "oldfieldtype": "Select", "options": "Fiscal Year", @@ -103,16 +96,25 @@ "read_only": 1 }, { - "fieldname": "year_start_date", + "fieldname": "period_end_date", + "fieldtype": "Date", + "label": "Period End Date", + "reqd": 1 + }, + { + "fieldname": "period_start_date", "fieldtype": "Date", - "label": "Year Start Date" + "label": "Period Start Date", + "oldfieldname": "posting_date", + "oldfieldtype": "Date", + "reqd": 1 } ], "icon": "fa fa-file-text", "idx": 1, "is_submittable": 1, "links": [], - "modified": "2023-09-11 20:19:11.810533", + "modified": "2024-09-15 17:22:45.291628", "modified_by": "Administrator", "module": "Accounts", "name": "Period Closing Voucher", @@ -148,7 +150,7 @@ "write": 1 } ], - "search_fields": "posting_date, fiscal_year", + "search_fields": "fiscal_year, period_start_date, period_end_date", "sort_field": "modified", "sort_order": "DESC", "states": [], 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 9bc110d243ef..7e0145e91a4e 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -2,15 +2,20 @@ # License: GNU General Public License v3. See license.txt +import copy + import frappe from frappe import _ from frappe.query_builder.functions import Sum -from frappe.utils import add_days, flt +from frappe.utils import add_days, flt, formatdate, getdate +from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import ( + make_closing_entries, +) from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, ) -from erpnext.accounts.utils import get_account_currency, get_fiscal_year, validate_fiscal_year +from erpnext.accounts.utils import get_account_currency, get_fiscal_year from erpnext.controllers.accounts_controller import AccountsController @@ -29,383 +34,475 @@ class PeriodClosingVoucher(AccountsController): error_message: DF.Text | None fiscal_year: DF.Link gle_processing_status: DF.Literal["In Progress", "Completed", "Failed"] - posting_date: DF.Date + period_end_date: DF.Date + period_start_date: DF.Date remarks: DF.SmallText transaction_date: DF.Date | None - year_start_date: DF.Date | None # end: auto-generated types def validate(self): - self.validate_account_head() - self.validate_posting_date() - - def on_submit(self): - self.db_set("gle_processing_status", "In Progress") - get_opening_entries = False - - if not frappe.db.exists( - "Period Closing Voucher", {"company": self.company, "docstatus": 1, "name": ("!=", self.name)} - ): - get_opening_entries = True - - self.make_gl_entries(get_opening_entries=get_opening_entries) + self.validate_start_and_end_date() + self.check_if_previous_year_closed() + self.block_if_future_closing_voucher_exists() + self.check_closing_account_type() + self.check_closing_account_currency() - def on_cancel(self): - self.validate_future_closing_vouchers() - self.db_set("gle_processing_status", "In Progress") - self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") - gle_count = frappe.db.count( - "GL Entry", - {"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0}, + def validate_start_and_end_date(self): + self.fy_start_date, self.fy_end_date = frappe.db.get_value( + "Fiscal Year", self.fiscal_year, ["year_start_date", "year_end_date"] ) - if gle_count > 5000: - frappe.enqueue( - make_reverse_gl_entries, - voucher_type="Period Closing Voucher", - voucher_no=self.name, - queue="long", - enqueue_after_commit=True, - ) - frappe.msgprint( - _("The GL Entries will be cancelled in the background, it can take a few minutes."), - alert=True, - ) - else: - make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name) - - self.delete_closing_entries() - - def validate_future_closing_vouchers(self): - if frappe.db.exists( - "Period Closing Voucher", - {"posting_date": (">", self.posting_date), "docstatus": 1, "company": self.company}, - ): - frappe.throw( - _( - "You can not cancel this Period Closing Voucher, please cancel the future Period Closing Vouchers first" - ) - ) - - def delete_closing_entries(self): - closing_balance = frappe.qb.DocType("Account Closing Balance") - frappe.qb.from_(closing_balance).delete().where( - closing_balance.period_closing_voucher == self.name - ).run() - - def validate_account_head(self): - closing_account_type = frappe.get_cached_value("Account", self.closing_account_head, "root_type") - - if closing_account_type not in ["Liability", "Equity"]: - frappe.throw( - _("Closing Account {0} must be of type Liability / Equity").format(self.closing_account_head) - ) - - account_currency = get_account_currency(self.closing_account_head) - company_currency = frappe.get_cached_value("Company", self.company, "default_currency") - if account_currency != company_currency: - frappe.throw(_("Currency of the Closing Account must be {0}").format(company_currency)) - def validate_posting_date(self): - validate_fiscal_year( - self.posting_date, self.fiscal_year, self.company, label=_("Posting Date"), doc=self + prev_closed_period_end_date = get_previous_closed_period_in_current_year( + self.fiscal_year, self.company + ) + valid_start_date = ( + add_days(prev_closed_period_end_date, 1) if prev_closed_period_end_date else self.fy_start_date ) - self.year_start_date = get_fiscal_year(self.posting_date, self.fiscal_year, company=self.company)[1] + if getdate(self.period_start_date) != getdate(valid_start_date): + frappe.throw(_("Period Start Date must be {0}").format(formatdate(valid_start_date))) - self.check_if_previous_year_closed() + if getdate(self.period_start_date) > getdate(self.period_end_date): + frappe.throw(_("Period Start Date cannot be greater than Period End Date")) - pcv = frappe.qb.DocType("Period Closing Voucher") - existing_entry = ( - frappe.qb.from_(pcv) - .select(pcv.name) - .where( - (pcv.posting_date >= self.posting_date) - & (pcv.fiscal_year == self.fiscal_year) - & (pcv.docstatus == 1) - & (pcv.company == self.company) - ) - .run() - ) - - if existing_entry and existing_entry[0][0]: - frappe.throw( - _("Another Period Closing Entry {0} has been made after {1}").format( - existing_entry[0][0], self.posting_date - ) - ) + if getdate(self.period_end_date) > getdate(self.fy_end_date): + frappe.throw(_("Period End Date cannot be greater than Fiscal Year End Date")) def check_if_previous_year_closed(self): - last_year_closing = add_days(self.year_start_date, -1) + last_year_closing = add_days(self.fy_start_date, -1) previous_fiscal_year = get_fiscal_year(last_year_closing, company=self.company, boolean=True) if not previous_fiscal_year: return previous_fiscal_year_start_date = previous_fiscal_year[0][1] - if not frappe.db.exists( + gle_exists_in_previous_year = frappe.db.exists( "GL Entry", { "posting_date": ("between", [previous_fiscal_year_start_date, last_year_closing]), "company": self.company, "is_cancelled": 0, }, - ): + ) + if not gle_exists_in_previous_year: return - if not frappe.db.exists( + previous_fiscal_year_closed = frappe.db.exists( "Period Closing Voucher", { - "posting_date": ("between", [previous_fiscal_year_start_date, last_year_closing]), + "period_end_date": ("between", [previous_fiscal_year_start_date, last_year_closing]), "docstatus": 1, "company": self.company, }, - ): + ) + if not previous_fiscal_year_closed: frappe.throw(_("Previous Year is not closed, please close it first")) - def make_gl_entries(self, get_opening_entries=False): - gl_entries = self.get_gl_entries() - closing_entries = self.get_grouped_gl_entries(get_opening_entries=get_opening_entries) - if len(gl_entries + closing_entries) > 3000: - frappe.enqueue( - process_gl_entries, - gl_entries=gl_entries, - voucher_name=self.name, - timeout=3000, + def block_if_future_closing_voucher_exists(self): + future_closing_voucher = self.get_future_closing_voucher() + if future_closing_voucher and future_closing_voucher[0][0]: + action = "cancel" if self.docstatus == 2 else "create" + frappe.throw( + _( + "You cannot {0} this document because another Period Closing Entry {1} exists after {2}" + ).format(action, future_closing_voucher[0][0], self.period_end_date) ) - frappe.enqueue( - process_closing_entries, - gl_entries=gl_entries, - closing_entries=closing_entries, - voucher_name=self.name, - company=self.company, - closing_date=self.posting_date, - timeout=3000, + def get_future_closing_voucher(self): + return frappe.db.get_value( + "Period Closing Voucher", + {"period_end_date": (">", self.period_end_date), "docstatus": 1, "company": self.company}, + "name", + ) + + def check_closing_account_type(self): + closing_account_type = frappe.get_cached_value("Account", self.closing_account_head, "root_type") + + if closing_account_type not in ["Liability", "Equity"]: + frappe.throw( + _("Closing Account {0} must be of type Liability / Equity").format(self.closing_account_head) ) + def check_closing_account_currency(self): + account_currency = get_account_currency(self.closing_account_head) + company_currency = frappe.get_cached_value("Company", self.company, "default_currency") + if account_currency != company_currency: + frappe.throw(_("Currency of the Closing Account must be {0}").format(company_currency)) + + def on_submit(self): + self.db_set("gle_processing_status", "In Progress") + self.make_gl_entries() + + def on_cancel(self): + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") + self.block_if_future_closing_voucher_exists() + self.db_set("gle_processing_status", "In Progress") + self.cancel_gl_entries() + + def make_gl_entries(self): + if self.get_gle_count_in_selected_period() > 5000: + frappe.enqueue( + process_gl_and_closing_entries, + doc=self, + timeout=1800, + ) frappe.msgprint( - _("The GL Entries will be processed in the background, it can take a few minutes."), + _( + "The GL Entries and closing balances will be processed in the background, it can take a few minutes." + ), alert=True, ) else: - process_gl_entries(gl_entries, self.name) - process_closing_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date) + process_gl_and_closing_entries(self) - def get_grouped_gl_entries(self, get_opening_entries=False): - closing_entries = [] - for acc in self.get_balances_based_on_dimensions( - group_by_account=True, for_aggregation=True, get_opening_entries=get_opening_entries - ): - closing_entries.append(self.get_closing_entries(acc)) - - return closing_entries - - def get_gl_entries(self): - gl_entries = [] - - # pl account - for acc in self.get_balances_based_on_dimensions( - group_by_account=True, report_type="Profit and Loss" - ): - if flt(acc.bal_in_company_currency): - gl_entries.append(self.get_gle_for_pl_account(acc)) + def get_gle_count_in_selected_period(self): + return frappe.db.count( + "GL Entry", + { + "posting_date": ["between", [self.period_start_date, self.period_end_date]], + "company": self.company, + "is_cancelled": 0, + }, + ) - # closing liability account - for acc in self.get_balances_based_on_dimensions( - group_by_account=False, report_type="Profit and Loss" - ): - if flt(acc.bal_in_company_currency): - gl_entries.append(self.get_gle_for_closing_account(acc)) + def get_pcv_gl_entries(self): + self.pl_accounts_reverse_gle = [] + self.closing_account_gle = [] + + pl_account_balances = self.get_account_balances_based_on_dimensions(report_type="Profit and Loss") + for dimensions, account_balances in pl_account_balances.items(): + for acc, balances in account_balances.items(): + balance_in_company_currency = flt(balances.debit) - flt(balances.credit) + if balance_in_company_currency and acc != "balances": + self.pl_accounts_reverse_gle.append( + self.get_gle_for_pl_account(acc, balances, dimensions) + ) + + # closing liability account + self.closing_account_gle.append( + self.get_gle_for_closing_account(account_balances["balances"], dimensions) + ) - return gl_entries + return self.pl_accounts_reverse_gle + self.closing_account_gle - def get_gle_for_pl_account(self, acc): - gl_entry = self.get_gl_dict( + def get_gle_for_pl_account(self, acc, balances, dimensions): + balance_in_account_currency = flt(balances.debit_in_account_currency) - flt( + balances.credit_in_account_currency + ) + balance_in_company_currency = flt(balances.debit) - flt(balances.credit) + gl_entry = frappe._dict( { "company": self.company, - "closing_date": self.posting_date, - "account": acc.account, - "cost_center": acc.cost_center, - "finance_book": acc.finance_book, - "account_currency": acc.account_currency, - "debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) - if flt(acc.bal_in_account_currency) < 0 - else 0, - "debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0, - "credit_in_account_currency": abs(flt(acc.bal_in_account_currency)) - if flt(acc.bal_in_account_currency) > 0 + "posting_date": self.period_end_date, + "account": acc, + "account_currency": balances.account_currency, + "debit_in_account_currency": abs(balance_in_account_currency) + if balance_in_account_currency < 0 else 0, - "credit": abs(flt(acc.bal_in_company_currency)) - if flt(acc.bal_in_company_currency) > 0 + "debit": abs(balance_in_company_currency) if balance_in_company_currency < 0 else 0, + "credit_in_account_currency": abs(balance_in_account_currency) + if balance_in_account_currency > 0 else 0, + "credit": abs(balance_in_company_currency) if balance_in_company_currency > 0 else 0, "is_period_closing_voucher_entry": 1, - }, - item=acc, + "voucher_type": "Period Closing Voucher", + "voucher_no": self.name, + "fiscal_year": self.fiscal_year, + "remarks": self.remarks, + "is_opening": "No", + } ) - self.update_default_dimensions(gl_entry, acc) + self.update_default_dimensions(gl_entry, dimensions) return gl_entry - def get_gle_for_closing_account(self, acc): - gl_entry = self.get_gl_dict( + def get_gle_for_closing_account(self, dimension_balance, dimensions): + balance_in_account_currency = flt(dimension_balance.balance_in_account_currency) + balance_in_company_currency = flt(dimension_balance.balance_in_company_currency) + gl_entry = frappe._dict( { "company": self.company, - "closing_date": self.posting_date, + "posting_date": self.period_end_date, "account": self.closing_account_head, - "cost_center": acc.cost_center, - "finance_book": acc.finance_book, - "account_currency": acc.account_currency, - "debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) - if flt(acc.bal_in_account_currency) > 0 - else 0, - "debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0, - "credit_in_account_currency": abs(flt(acc.bal_in_account_currency)) - if flt(acc.bal_in_account_currency) < 0 + "account_currency": frappe.db.get_value( + "Account", self.closing_account_head, "account_currency" + ), + "debit_in_account_currency": balance_in_account_currency + if balance_in_account_currency > 0 else 0, - "credit": abs(flt(acc.bal_in_company_currency)) - if flt(acc.bal_in_company_currency) < 0 + "debit": balance_in_company_currency if balance_in_company_currency > 0 else 0, + "credit_in_account_currency": abs(balance_in_account_currency) + if balance_in_account_currency < 0 else 0, + "credit": abs(balance_in_company_currency) if balance_in_company_currency < 0 else 0, "is_period_closing_voucher_entry": 1, - }, - item=acc, + "voucher_type": "Period Closing Voucher", + "voucher_no": self.name, + "fiscal_year": self.fiscal_year, + "remarks": self.remarks, + "is_opening": "No", + } ) - self.update_default_dimensions(gl_entry, acc) + self.update_default_dimensions(gl_entry, dimensions) return gl_entry - def get_closing_entries(self, acc): - closing_entry = self.get_gl_dict( - { - "company": self.company, - "closing_date": self.posting_date, - "period_closing_voucher": self.name, - "account": acc.account, - "cost_center": acc.cost_center, - "finance_book": acc.finance_book, - "account_currency": acc.account_currency, - "debit_in_account_currency": flt(acc.debit_in_account_currency), - "debit": flt(acc.debit), - "credit_in_account_currency": flt(acc.credit_in_account_currency), - "credit": flt(acc.credit), - }, - item=acc, + def update_default_dimensions(self, gl_entry, dimensions): + for i, dimension in enumerate(self.accounting_dimension_fields): + gl_entry[dimension] = dimensions[i] + + def get_account_balances_based_on_dimensions(self, report_type): + """Get balance for dimension-wise pl accounts""" + self.get_accounting_dimension_fields() + acc_bal_dict = frappe._dict() + gl_entries = [] + + with frappe.db.unbuffered_cursor(): + gl_entries = self.get_gl_entries_for_current_period(report_type, as_iterator=True) + for gle in gl_entries: + acc_bal_dict = self.set_account_balance_dict(gle, acc_bal_dict) + + if report_type == "Balance Sheet" and self.is_first_period_closing_voucher(): + opening_entries = self.get_gl_entries_for_current_period(report_type, only_opening_entries=True) + for gle in opening_entries: + acc_bal_dict = self.set_account_balance_dict(gle, acc_bal_dict) + + return acc_bal_dict + + def get_accounting_dimension_fields(self): + default_dimensions = ["cost_center", "finance_book", "project"] + self.accounting_dimension_fields = default_dimensions + get_accounting_dimensions() + + def get_gl_entries_for_current_period(self, report_type, only_opening_entries=False, as_iterator=False): + date_condition = "" + if only_opening_entries: + date_condition = "is_opening = 'Yes'" + else: + date_condition = f"posting_date BETWEEN '{self.period_start_date}' AND '{self.period_end_date}' and is_opening = 'No'" + + # nosemgrep + return frappe.db.sql( + """ + SELECT + name, + posting_date, + account, + account_currency, + debit_in_account_currency, + credit_in_account_currency, + debit, + credit, + {} + FROM `tabGL Entry` + WHERE + {} + AND company = %s + AND voucher_type != 'Period Closing Voucher' + AND EXISTS(SELECT name FROM `tabAccount` WHERE name = account AND report_type = %s) + AND is_cancelled = 0 + """.format( + ", ".join(self.accounting_dimension_fields), + date_condition, + ), + (self.company, report_type), + as_dict=1, + as_iterator=as_iterator, ) - for dimension in self.accounting_dimensions: - closing_entry.update({dimension: acc.get(dimension)}) + def set_account_balance_dict(self, gle, acc_bal_dict): + key = self.get_key(gle) + + acc_bal_dict.setdefault(key, frappe._dict()).setdefault( + gle.account, + frappe._dict( + { + "debit_in_account_currency": 0, + "credit_in_account_currency": 0, + "debit": 0, + "credit": 0, + "account_currency": gle.account_currency, + } + ), + ) - return closing_entry + acc_bal_dict[key][gle.account].debit_in_account_currency += flt(gle.debit_in_account_currency) + acc_bal_dict[key][gle.account].credit_in_account_currency += flt(gle.credit_in_account_currency) + acc_bal_dict[key][gle.account].debit += flt(gle.debit) + acc_bal_dict[key][gle.account].credit += flt(gle.credit) + + # dimension-wise total balances + acc_bal_dict[key].setdefault( + "balances", + frappe._dict( + { + "balance_in_account_currency": 0, + "balance_in_company_currency": 0, + } + ), + ) - def update_default_dimensions(self, gl_entry, acc): - if not self.accounting_dimensions: - self.accounting_dimensions = get_accounting_dimensions() + balance_in_account_currency = flt(gle.debit_in_account_currency) - flt(gle.credit_in_account_currency) + balance_in_company_currency = flt(gle.debit) - flt(gle.credit) - for dimension in self.accounting_dimensions: - gl_entry.update({dimension: acc.get(dimension)}) + acc_bal_dict[key]["balances"].balance_in_account_currency += balance_in_account_currency + acc_bal_dict[key]["balances"].balance_in_company_currency += balance_in_company_currency - def get_balances_based_on_dimensions( - self, group_by_account=False, report_type=None, for_aggregation=False, get_opening_entries=False - ): - """Get balance for dimension-wise pl accounts""" + return acc_bal_dict - qb_dimension_fields = ["cost_center", "finance_book", "project"] + def get_key(self, gle): + return tuple([gle.get(dimension) for dimension in self.accounting_dimension_fields]) - self.accounting_dimensions = get_accounting_dimensions() - for dimension in self.accounting_dimensions: - qb_dimension_fields.append(dimension) + def get_account_closing_balances(self): + pl_closing_entries = self.get_closing_entries_for_pl_accounts() + bs_closing_entries = self.get_closing_entries_for_balance_sheet_accounts() + closing_entries_for_closing_account = self.get_closing_entries_for_closing_account() + closing_entries = pl_closing_entries + bs_closing_entries + closing_entries_for_closing_account + return closing_entries - if group_by_account: - qb_dimension_fields.append("account") + def get_closing_entries_for_pl_accounts(self): + closing_entries = copy.deepcopy(self.pl_accounts_reverse_gle) + for d in self.pl_accounts_reverse_gle: + # reverse debit and credit + gle_copy = copy.deepcopy(d) + gle_copy.debit = d.credit + gle_copy.credit = d.debit + gle_copy.debit_in_account_currency = d.credit_in_account_currency + gle_copy.credit_in_account_currency = d.debit_in_account_currency + gle_copy.is_period_closing_voucher_entry = 0 + gle_copy.period_closing_voucher = self.name + closing_entries.append(gle_copy) - account_filters = { - "company": self.company, - "is_group": 0, - } + return closing_entries - if report_type: - account_filters.update({"report_type": report_type}) + def get_closing_entries_for_balance_sheet_accounts(self): + closing_entries = [] + balance_sheet_account_balances = self.get_account_balances_based_on_dimensions( + report_type="Balance Sheet" + ) - accounts = frappe.get_all("Account", filters=account_filters, pluck="name") + for dimensions, account_balances in balance_sheet_account_balances.items(): + for acc, balances in account_balances.items(): + balance_in_company_currency = flt(balances.debit) - flt(balances.credit) + if acc != "balances" and balance_in_company_currency: + closing_entries.append(self.get_closing_entry(acc, balances, dimensions)) - gl_entry = frappe.qb.DocType("GL Entry") - query = frappe.qb.from_(gl_entry).select(gl_entry.account, gl_entry.account_currency) + return closing_entries - if not for_aggregation: - query = query.select( - (Sum(gl_entry.debit_in_account_currency) - Sum(gl_entry.credit_in_account_currency)).as_( - "bal_in_account_currency" - ), - (Sum(gl_entry.debit) - Sum(gl_entry.credit)).as_("bal_in_company_currency"), - ) - else: - query = query.select( - (Sum(gl_entry.debit_in_account_currency)).as_("debit_in_account_currency"), - (Sum(gl_entry.credit_in_account_currency)).as_("credit_in_account_currency"), - (Sum(gl_entry.debit)).as_("debit"), - (Sum(gl_entry.credit)).as_("credit"), - ) + def get_closing_entry(self, account, balances, dimensions): + closing_entry = frappe._dict( + { + "company": self.company, + "closing_date": self.period_end_date, + "period_closing_voucher": self.name, + "account": account, + "account_currency": balances.account_currency, + "debit_in_account_currency": flt(balances.debit_in_account_currency), + "debit": flt(balances.debit), + "credit_in_account_currency": flt(balances.credit_in_account_currency), + "credit": flt(balances.credit), + "is_period_closing_voucher_entry": 0, + } + ) + self.update_default_dimensions(closing_entry, dimensions) + return closing_entry - for dimension in qb_dimension_fields: - query = query.select(gl_entry[dimension]) + def get_closing_entries_for_closing_account(self): + closing_entries = copy.deepcopy(self.closing_account_gle) + for d in closing_entries: + d.period_closing_voucher = self.name + + return closing_entries - query = query.where( - (gl_entry.company == self.company) - & (gl_entry.is_cancelled == 0) - & (gl_entry.account.isin(accounts)) + def is_first_period_closing_voucher(self): + first_pcv = frappe.db.get_value( + "Period Closing Voucher", + {"company": self.company, "docstatus": 1}, + "name", + order_by="period_end_date asc", ) - if get_opening_entries: - query = query.where( - gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date) - | gl_entry.is_opening - == "Yes" + if not first_pcv or first_pcv == self.name: + return True + + def cancel_gl_entries(self): + if self.get_gle_count_against_current_pcv() > 5000: + frappe.enqueue( + process_cancellation, + voucher_type="Period Closing Voucher", + voucher_no=self.name, + queue="long", + enqueue_after_commit=True, ) - else: - query = query.where( - gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date) - & gl_entry.is_opening - == "No" + frappe.msgprint( + _("The GL Entries will be cancelled in the background, it can take a few minutes."), + alert=True, ) + else: + process_cancellation(voucher_type="Period Closing Voucher", voucher_no=self.name) - if for_aggregation: - query = query.where(gl_entry.voucher_type != "Period Closing Voucher") - - for dimension in qb_dimension_fields: - query = query.groupby(gl_entry[dimension]) - - return query.run(as_dict=1) + def get_gle_count_against_current_pcv(self): + return frappe.db.count( + "GL Entry", + {"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0}, + ) -def process_gl_entries(gl_entries, voucher_name): +def process_gl_and_closing_entries(doc): from erpnext.accounts.general_ledger import make_gl_entries try: + gl_entries = doc.get_pcv_gl_entries() if gl_entries: make_gl_entries(gl_entries, merge_entries=False) - frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Completed") - except Exception as e: - frappe.db.rollback() - frappe.log_error(e) - frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Failed") + closing_entries = doc.get_account_closing_balances() + make_closing_entries(closing_entries, doc.name, doc.company, doc.period_end_date) -def process_closing_entries(gl_entries, closing_entries, voucher_name, company, closing_date): - from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import ( - make_closing_entries, - ) - - try: - if gl_entries + closing_entries: - make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date) + frappe.db.set_value(doc.doctype, doc.name, "gle_processing_status", "Completed") except Exception as e: frappe.db.rollback() frappe.log_error(e) + frappe.db.set_value(doc.doctype, doc.name, "gle_processing_status", "Failed") -def make_reverse_gl_entries(voucher_type, voucher_no): +def process_cancellation(voucher_type, voucher_no): from erpnext.accounts.general_ledger import make_reverse_gl_entries try: make_reverse_gl_entries(voucher_type=voucher_type, voucher_no=voucher_no) + delete_closing_entries(voucher_no) frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Completed") except Exception as e: frappe.db.rollback() frappe.log_error(e) frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Failed") + + +def delete_closing_entries(voucher_no): + closing_balance = frappe.qb.DocType("Account Closing Balance") + frappe.qb.from_(closing_balance).delete().where( + closing_balance.period_closing_voucher == voucher_no + ).run() + + +@frappe.whitelist() +def get_period_start_end_date(fiscal_year, company): + fy_start_date, fy_end_date = frappe.db.get_value( + "Fiscal Year", fiscal_year, ["year_start_date", "year_end_date"] + ) + prev_closed_period_end_date = get_previous_closed_period_in_current_year(fiscal_year, company) + period_start_date = ( + add_days(prev_closed_period_end_date, 1) if prev_closed_period_end_date else fy_start_date + ) + return period_start_date, fy_end_date + + +def get_previous_closed_period_in_current_year(fiscal_year, company): + prev_closed_period_end_date = frappe.db.get_value( + "Period Closing Voucher", + filters={ + "company": company, + "fiscal_year": fiscal_year, + "docstatus": 1, + }, + fieldname=["period_end_date"], + order_by="period_end_date desc", + ) + return prev_closed_period_end_date 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 1bd565e1b361..e9d65f7f8560 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 @@ -317,16 +317,18 @@ def test_closing_balance_with_dimensions_and_test_reposting_entry(self): repost_doc.posting_date = today() repost_doc.save() - def make_period_closing_voucher(self, posting_date=None, submit=True): + def make_period_closing_voucher(self, posting_date, submit=True): surplus_account = create_account() cost_center = create_cost_center("Test Cost Center 1") + fy = get_fiscal_year(posting_date, company="Test PCV Company") pcv = frappe.get_doc( { "doctype": "Period Closing Voucher", "transaction_date": posting_date or today(), - "posting_date": posting_date or today(), + "period_start_date": fy[1], + "period_end_date": fy[2], "company": "Test PCV Company", - "fiscal_year": get_fiscal_year(today(), company="Test PCV Company")[0], + "fiscal_year": fy[0], "cost_center": cost_center, "closing_account_head": surplus_account, "remarks": "test", 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 68a85f7ee963..7504c79141bb 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js @@ -80,8 +80,10 @@ frappe.ui.form.on("POS Closing Entry", { ) { reset_values(frm); frappe.run_serially([ + () => frappe.dom.freeze(__("Loading Invoices! Please Wait...")), () => frm.trigger("set_opening_amounts"), () => frm.trigger("get_pos_invoices"), + () => frappe.dom.unfreeze(), ]); } }, @@ -145,7 +147,7 @@ frappe.ui.form.on("POS Closing Entry", { 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_payments(doc, frm, false); refresh_taxes(doc, frm); refresh_fields(frm); set_html_data(frm); @@ -170,7 +172,7 @@ function set_form_data(data, frm) { frm.doc.grand_total += flt(d.grand_total); frm.doc.net_total += flt(d.net_total); frm.doc.total_quantity += flt(d.total_qty); - refresh_payments(d, frm); + refresh_payments(d, frm, true); refresh_taxes(d, frm); }); } @@ -184,7 +186,7 @@ function add_to_pos_transaction(d, frm) { }); } -function refresh_payments(d, frm) { +function refresh_payments(d, frm, is_new) { d.payments.forEach((p) => { const payment = frm.doc.payment_reconciliation.find( (pay) => pay.mode_of_payment === p.mode_of_payment @@ -194,7 +196,7 @@ function refresh_payments(d, frm) { } if (payment) { payment.expected_amount += flt(p.amount); - payment.closing_amount = payment.expected_amount; + if (is_new) payment.closing_amount = payment.expected_amount; payment.difference = payment.closing_amount - payment.expected_amount; } else { frm.add_child("payment_reconciliation", { diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py index 9faf8693a8a5..fda868cfe513 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py @@ -87,19 +87,15 @@ def validate_pos_invoices(self): as_dict=1, )[0] if pos_invoice.consolidated_invoice: - invalid_row.setdefault("msg", []).append( - _("POS Invoice is {}").format(frappe.bold("already consolidated")) - ) + invalid_row.setdefault("msg", []).append(_("POS Invoice is already consolidated")) invalid_rows.append(invalid_row) continue if pos_invoice.pos_profile != self.pos_profile: invalid_row.setdefault("msg", []).append( - _("POS Profile doesn't matches {}").format(frappe.bold(self.pos_profile)) + _("POS Profile doesn't match {}").format(frappe.bold(self.pos_profile)) ) if pos_invoice.docstatus != 1: - invalid_row.setdefault("msg", []).append( - _("POS Invoice is not {}").format(frappe.bold("submitted")) - ) + invalid_row.setdefault("msg", []).append(_("POS Invoice is not submitted")) if pos_invoice.owner != self.user: invalid_row.setdefault("msg", []).append( _("POS Invoice isn't created by user {}").format(frappe.bold(self.owner)) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js index a6e8bfa62869..6a537a2559ae 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js @@ -40,10 +40,24 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex }; }); + this.frm.set_query("item_code", "items", function (doc) { + return { + query: "erpnext.accounts.doctype.pos_invoice.pos_invoice.item_query", + filters: { + has_variants: ["=", 0], + is_sales_item: ["=", 1], + disabled: ["=", 0], + is_fixed_asset: ["=", 0], + pos_profile: ["=", doc.pos_profile], + }, + }; + }); + erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype); } onload_post_render(frm) { + super.onload_post_render(); this.pos_profile(frm); } @@ -51,7 +65,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex super.refresh(); if (doc.docstatus == 1 && !doc.is_return) { - this.frm.add_custom_button(__("Return"), this.make_sales_return, __("Create")); + this.frm.add_custom_button(__("Return"), this.make_sales_return.bind(this), __("Create")); this.frm.page.set_inner_btn_group_as_primary(__("Create")); } diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index d7b173667ecb..428611404941 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -48,6 +48,7 @@ "shipping_address", "company_address", "company_address_display", + "company_contact_person", "currency_and_price_list", "currency", "conversion_rate", @@ -1558,12 +1559,19 @@ "fieldname": "update_billed_amount_in_delivery_note", "fieldtype": "Check", "label": "Update Billed Amount in Delivery Note" + }, + { + "fieldname": "company_contact_person", + "fieldtype": "Link", + "label": "Company Contact Person", + "options": "Contact", + "print_hide": 1 } ], "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2024-03-20 16:00:34.268756", + "modified": "2024-11-26 13:10:50.309570", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index ef4db1dac980..ab5a4092c338 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -6,6 +6,7 @@ from frappe import _, bold from frappe.query_builder.functions import IfNull, Sum from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate +from frappe.utils.nestedset import get_descendants_of from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request @@ -15,6 +16,7 @@ update_multi_mode_option, ) from erpnext.accounts.party import get_due_date, get_party_account +from erpnext.controllers.queries import item_query as _item_query from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -30,12 +32,8 @@ class POSInvoice(SalesInvoice): from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule from erpnext.accounts.doctype.pos_invoice_item.pos_invoice_item import POSInvoiceItem from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail - from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import ( - SalesInvoiceAdvance, - ) - from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import ( - SalesInvoicePayment, - ) + from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import SalesInvoiceAdvance + from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import SalesInvoicePayment from erpnext.accounts.doctype.sales_invoice_timesheet.sales_invoice_timesheet import ( SalesInvoiceTimesheet, ) @@ -73,6 +71,7 @@ class POSInvoice(SalesInvoice): company: DF.Link company_address: DF.Link | None company_address_display: DF.SmallText | None + company_contact_person: DF.Link | None consolidated_invoice: DF.Link | None contact_display: DF.SmallText | None contact_email: DF.Data | None @@ -188,7 +187,7 @@ def __init__(self, *args, **kwargs): def validate(self): if not cint(self.is_pos): frappe.throw( - _("POS Invoice should have {} field checked.").format(frappe.bold("Include Payment")) + _("POS Invoice should have the field {0} checked.").format(frappe.bold(_("Include Payment"))) ) # run on validate method of selling controller @@ -449,7 +448,7 @@ def validate_payment_amount(self): if self.is_return and entry.amount > 0: frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx)) - if self.is_return: + if self.is_return and self.docstatus != 0: invoice_total = self.rounded_total or self.grand_total total_amount_in_payments = flt(total_amount_in_payments, self.precision("grand_total")) if total_amount_in_payments and total_amount_in_payments < invoice_total: @@ -837,3 +836,30 @@ def append_payment(payment_mode): ]: payment_mode = get_mode_of_payment_info(mode_of_payment, doc.company) append_payment(payment_mode[0]) + + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False): + if pos_profile := filters.get("pos_profile")[1]: + pos_profile = frappe.get_cached_doc("POS Profile", pos_profile) + if item_groups := get_item_group(pos_profile): + filters["item_group"] = ["in", tuple(item_groups)] + + del filters["pos_profile"] + + else: + filters.pop("pos_profile", None) + + return _item_query(doctype, txt, searchfield, start, page_len, filters, as_dict) + + +def get_item_group(pos_profile): + item_groups = [] + if pos_profile.get("item_groups"): + # Get items based on the item groups defined in the POS profile + for row in pos_profile.get("item_groups"): + item_groups.append(row.item_group) + item_groups.extend(get_descendants_of("Item Group", row.item_group)) + + return list(set(item_groups)) diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index f210a6434cf7..1dbc630e62ec 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -93,7 +93,7 @@ def test_discount_and_inclusive_tax(self): inv.save() - self.assertEqual(inv.net_total, 4298.25) + self.assertEqual(inv.net_total, 4298.24) self.assertEqual(inv.grand_total, 4900.00) def test_tax_calculation_with_multiple_items(self): diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index 2cf204dd3473..5bb43b3fa723 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -97,16 +97,15 @@ def validate_pos_invoice_status(self): return_against_status = frappe.db.get_value("POS Invoice", return_against, "status") if return_against_status != "Consolidated": # if return entry is not getting merged in the current pos closing and if it is not consolidated - bold_unconsolidated = frappe.bold("not Consolidated") - msg = _("Row #{}: Original Invoice {} of return invoice {} is {}.").format( - d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated - ) + msg = _( + "Row #{}: The original Invoice {} of return invoice {} is not consolidated." + ).format(d.idx, bold_return_against, bold_pos_invoice) msg += " " msg += _( - "Original invoice should be consolidated before or along with the return invoice." + "The original invoice should be consolidated before or along with the return invoice." ) msg += "

" - msg += _("You can add original invoice {} manually to proceed.").format( + msg += _("You can add the original invoice {} manually to proceed.").format( bold_return_against ) frappe.throw(msg) @@ -439,7 +438,9 @@ def split_invoices(invoices): if not item.serial_no and not item.serial_and_batch_bundle: continue - return_against_is_added = any(d for d in _invoices if d.pos_invoice == pos_invoice.return_against) + return_against_is_added = any( + d for d in _invoices if d and d[0].pos_invoice == pos_invoice.return_against + ) if return_against_is_added: break diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py index 20f6ddb5ba95..904d8e83b9c0 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py @@ -343,7 +343,7 @@ def test_consolidation_round_off_error_3(self): inv.load_from_db() consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice) self.assertEqual(consolidated_invoice.status, "Return") - self.assertEqual(consolidated_invoice.rounding_adjustment, -0.001) + self.assertEqual(consolidated_invoice.rounding_adjustment, -0.002) finally: frappe.set_user("Administrator") diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index 6f191c106c91..ee9dd2be8c35 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -419,7 +419,8 @@ "depends_on": "eval:doc.rate_or_discount==\"Rate\"", "fieldname": "rate", "fieldtype": "Currency", - "label": "Rate" + "label": "Rate", + "options": "currency" }, { "default": "0", @@ -647,7 +648,7 @@ "icon": "fa fa-gift", "idx": 1, "links": [], - "modified": "2024-05-17 13:16:34.496704", + "modified": "2024-09-16 18:14:51.314765", "modified_by": "Administrator", "module": "Accounts", "name": "Pricing Rule", @@ -709,4 +710,4 @@ "sort_order": "DESC", "states": [], "title_field": "title" -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index de29ec0f004a..73cb24838118 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -186,7 +186,8 @@ def validate_mandatory(self): if not self.priority: throw( _("As the field {0} is enabled, the field {1} is mandatory.").format( - frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority") + frappe.bold(_("Apply Discount on Discounted Rate")), + frappe.bold(_("Priority")), ) ) @@ -194,7 +195,7 @@ def validate_mandatory(self): throw( _( "As the field {0} is enabled, the value of the field {1} should be more than 1." - ).format(frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority")) + ).format(frappe.bold(_("Apply Discount on Discounted Rate")), frappe.bold(_("Priority"))) ) def validate_applicable_for_selling_or_buying(self): @@ -445,7 +446,20 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False): if isinstance(pricing_rule, str): pricing_rule = frappe.get_cached_doc("Pricing Rule", pricing_rule) update_pricing_rule_uom(pricing_rule, args) - pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule) or [] + fetch_other_item = True if pricing_rule.apply_rule_on_other else False + pricing_rule.apply_rule_on_other_items = ( + get_pricing_rule_items(pricing_rule, other_items=fetch_other_item) or [] + ) + + if pricing_rule.coupon_code_based == 1: + if not args.coupon_code: + return item_details + + coupon_code = frappe.db.get_value( + doctype="Coupon Code", filters={"pricing_rule": pricing_rule.name}, fieldname="name" + ) + if args.coupon_code != coupon_code: + continue if pricing_rule.get("suggestion"): continue @@ -472,9 +486,6 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False): pricing_rule.apply_rule_on_other_items ) - if pricing_rule.coupon_code_based == 1 and args.coupon_code is None: - return item_details - if not pricing_rule.validate_applied_rule: if pricing_rule.price_or_product_discount == "Price": apply_price_discount_rule(pricing_rule, item_details, args) diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 235fddf3ab39..965e2b267a3d 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -5,6 +5,7 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase, change_settings from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice @@ -14,7 +15,7 @@ from erpnext.stock.get_item_details import get_item_details -class TestPricingRule(unittest.TestCase): +class TestPricingRule(FrappeTestCase): def setUp(self): delete_existing_pricing_rules() setup_pricing_rule_data() @@ -1130,6 +1131,51 @@ def test_pricing_rule_for_product_free_item_rounded_qty_and_recursion(self): self.assertEqual(so.items[1].item_code, "_Test Item") self.assertEqual(so.items[1].qty, 3) + so = make_sales_order(item_code="_Test Item", qty=5, do_not_submit=1) + so.items[0].qty = 1 + del so.items[-1] + so.save() + self.assertEqual(len(so.items), 1) + + def test_pricing_rule_for_product_free_item_round_free_qty(self): + frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule") + test_record = { + "doctype": "Pricing Rule", + "title": "_Test Pricing Rule", + "apply_on": "Item Code", + "currency": "USD", + "items": [ + { + "item_code": "_Test Item", + } + ], + "selling": 1, + "rate": 0, + "min_qty": 100, + "max_qty": 0, + "price_or_product_discount": "Product", + "same_item": 1, + "free_qty": 10, + "round_free_qty": 1, + "is_recursive": 1, + "recurse_for": 100, + "company": "_Test Company", + } + frappe.get_doc(test_record.copy()).insert() + + # With pricing rule + so = make_sales_order(item_code="_Test Item", qty=100) + so.load_from_db() + self.assertEqual(so.items[1].is_free_item, 1) + self.assertEqual(so.items[1].item_code, "_Test Item") + self.assertEqual(so.items[1].qty, 10) + + so = make_sales_order(item_code="_Test Item", qty=150) + so.load_from_db() + self.assertEqual(so.items[1].is_free_item, 1) + self.assertEqual(so.items[1].item_code, "_Test Item") + self.assertEqual(so.items[1].qty, 10) + def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(self): frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1") frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2") diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 9c7911d7caee..551eaa3d1ced 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -486,7 +486,7 @@ def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules, row_item): continue stock_qty = row.get("qty") * (row.get("conversion_factor") or 1.0) - amount = stock_qty * (row.get("price_list_rate") or row.get("rate")) + amount = stock_qty * (flt(row.get("price_list_rate")) or flt(row.get("rate"))) pricing_rules = filter_pricing_rules_for_qty_amount(stock_qty, amount, pricing_rules, row) if pricing_rules and pricing_rules[0]: @@ -651,11 +651,23 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None): qty = pricing_rule.free_qty or 1 if pricing_rule.is_recursive: - transaction_qty = (args.get("qty") if args else doc.total_qty) - pricing_rule.apply_recursion_over - if transaction_qty: + transaction_qty = sum( + [ + row.qty + for row in doc.items + if not row.is_free_item + and row.item_code == args.item_code + and row.pricing_rules == args.pricing_rules + ] + ) + transaction_qty = transaction_qty - pricing_rule.apply_recursion_over + if transaction_qty and transaction_qty > 0: qty = flt(transaction_qty) * qty / pricing_rule.recurse_for if pricing_rule.round_free_qty: - qty = math.floor(qty) + qty = (flt(transaction_qty) // pricing_rule.recurse_for) * (pricing_rule.free_qty or 1) + + if not qty: + return free_item_data_args = { "item_code": free_item, @@ -725,14 +737,11 @@ def get_pricing_rule_items(pr_doc, other_items=False) -> list: def validate_coupon_code(coupon_name): coupon = frappe.get_doc("Coupon Code", coupon_name) - - if coupon.valid_from: - if coupon.valid_from > getdate(today()): - frappe.throw(_("Sorry, this coupon code's validity has not started")) - elif coupon.valid_upto: - if coupon.valid_upto < getdate(today()): - frappe.throw(_("Sorry, this coupon code's validity has expired")) - elif coupon.used >= coupon.maximum_use: + if coupon.valid_from and coupon.valid_from > getdate(today()): + frappe.throw(_("Sorry, this coupon code's validity has not started")) + elif coupon.valid_upto and coupon.valid_upto < getdate(today()): + frappe.throw(_("Sorry, this coupon code's validity has expired")) + elif coupon.maximum_use and coupon.used >= coupon.maximum_use: frappe.throw(_("Sorry, this coupon code is no longer valid")) diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js index 0f52a4d998e4..d72c4724690a 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js +++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js @@ -20,6 +20,17 @@ frappe.ui.form.on("Process Payment Reconciliation", { }, }; }); + + frm.set_query("default_advance_account", function (doc) { + return { + filters: { + company: doc.company, + is_group: 0, + account_type: doc.party_type == "Customer" ? "Receivable" : "Payable", + root_type: doc.party_type == "Customer" ? "Liability" : "Asset", + }, + }; + }); frm.set_query("cost_center", function (doc) { return { filters: { @@ -102,6 +113,7 @@ frappe.ui.form.on("Process Payment Reconciliation", { company(frm) { frm.set_value("party", ""); frm.set_value("receivable_payable_account", ""); + frm.set_value("default_advance_account", ""); }, party_type(frm) { frm.set_value("party", ""); @@ -109,6 +121,7 @@ frappe.ui.form.on("Process Payment Reconciliation", { party(frm) { frm.set_value("receivable_payable_account", ""); + frm.set_value("default_advance_account", ""); if (!frm.doc.receivable_payable_account && frm.doc.party_type && frm.doc.party) { return frappe.call({ method: "erpnext.accounts.party.get_party_account", @@ -116,10 +129,16 @@ frappe.ui.form.on("Process Payment Reconciliation", { company: frm.doc.company, party_type: frm.doc.party_type, party: frm.doc.party, + include_advance: 1, }, callback: (r) => { if (!r.exc && r.message) { - frm.set_value("receivable_payable_account", r.message); + if (typeof r.message === "string") { + frm.set_value("receivable_payable_account", r.message); + } else if (Array.isArray(r.message)) { + frm.set_value("receivable_payable_account", r.message[0]); + frm.set_value("default_advance_account", r.message[1]); + } } frm.refresh(); }, diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json index 1a1ab4d800e8..0511571d754a 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json +++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json @@ -13,6 +13,7 @@ "column_break_io6c", "party", "receivable_payable_account", + "default_advance_account", "filter_section", "from_invoice_date", "to_invoice_date", @@ -141,12 +142,23 @@ { "fieldname": "section_break_a8yx", "fieldtype": "Section Break" + }, + { + "depends_on": "eval:doc.party", + "description": "Only 'Payment Entries' made against this advance account are supported.", + "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account", + "fieldname": "default_advance_account", + "fieldtype": "Link", + "label": "Default Advance Account", + "mandatory_depends_on": "doc.party_type", + "options": "Account", + "reqd": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-08-11 10:56:51.699137", + "modified": "2024-08-27 14:48:56.715320", "modified_by": "Administrator", "module": "Accounts", "name": "Process Payment Reconciliation", @@ -180,4 +192,4 @@ "sort_order": "DESC", "states": [], "title_field": "company" -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py index 5048fc5e25e6..882a2c4ad1cc 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py +++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py @@ -23,6 +23,7 @@ class ProcessPaymentReconciliation(Document): bank_cash_account: DF.Link | None company: DF.Link cost_center: DF.Link | None + default_advance_account: DF.Link error_log: DF.LongText | None from_invoice_date: DF.Date | None from_payment_date: DF.Date | None @@ -101,6 +102,7 @@ def get_pr_instance(doc: str): "party_type", "party", "receivable_payable_account", + "default_advance_account", "from_invoice_date", "to_invoice_date", "from_payment_date", diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json index 22be52992803..763607c22a16 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json @@ -25,6 +25,7 @@ "payment_terms_template", "sales_partner", "sales_person", + "show_remarks", "based_on_payment_terms", "section_break_3", "customer_collection", @@ -390,10 +391,16 @@ "fieldname": "ignore_cr_dr_notes", "fieldtype": "Check", "label": "Ignore System Generated Credit / Debit Notes" + }, + { + "default": "0", + "fieldname": "show_remarks", + "fieldtype": "Check", + "label": "Show Remarks" } ], "links": [], - "modified": "2024-08-13 10:41:18.381165", + "modified": "2024-10-18 17:51:39.108481", "modified_by": "Administrator", "module": "Accounts", "name": "Process Statement Of Accounts", diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 509199ccae64..bf1c8c0b66ef 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -70,6 +70,7 @@ class ProcessStatementOfAccounts(Document): sales_person: DF.Link | None sender: DF.Link | None show_net_values_in_party_account: DF.Check + show_remarks: DF.Check start_date: DF.Date | None subject: DF.Data | None terms_and_conditions: DF.Link | None @@ -187,6 +188,7 @@ def get_common_filters(doc): "finance_book": doc.finance_book if doc.finance_book else None, "account": [doc.account] if doc.account else None, "cost_center": [cc.cost_center_name for cc in doc.cost_center], + "show_remarks": doc.show_remarks, } ) @@ -472,6 +474,7 @@ def send_emails(document_name, from_scheduler=False, posting_date=None): reference_doctype="Process Statement Of Accounts", reference_name=document_name, attachments=attachments, + expose_recipients="header", ) if doc.enable_auto_email and from_scheduler: diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py index 86bd21355153..4cc87394b4f7 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py @@ -5,6 +5,8 @@ import frappe from frappe import _ from frappe.model.document import Document +from frappe.query_builder import Criterion +from frappe.query_builder.functions import IfNull pricing_rule_fields = [ "apply_on", @@ -162,22 +164,50 @@ def validate_pricing_rules(self): if self.is_new(): return - transaction_exists = False - docnames = [] + invalid_pricing_rule = self.get_invalid_pricing_rules() - # If user has changed applicable for - if self.get_doc_before_save() and self.get_doc_before_save().applicable_for == self.applicable_for: + if not invalid_pricing_rule: return - docnames = frappe.get_all("Pricing Rule", filters={"promotional_scheme": self.name}) + if frappe.db.exists( + "Pricing Rule Detail", + { + "pricing_rule": ["in", invalid_pricing_rule], + "docstatus": ["<", 2], + }, + ): + raise_for_transaction_exists(self.name) + + for doc in invalid_pricing_rule: + frappe.delete_doc("Pricing Rule", doc) + + frappe.msgprint( + _("The following invalid Pricing Rules are deleted:") + + "

  • " + + "
  • ".join(invalid_pricing_rule) + + "
" + ) + + def get_invalid_pricing_rules(self): + pr = frappe.qb.DocType("Pricing Rule") + conditions = [] + conditions.append(pr.promotional_scheme == self.name) + + if self.applicable_for: + applicable_for = frappe.scrub(self.applicable_for) + applicable_for_list = [d.get(applicable_for) for d in self.get(applicable_for)] - for docname in docnames: - if frappe.db.exists("Pricing Rule Detail", {"pricing_rule": docname.name, "docstatus": ("<", 2)}): - raise_for_transaction_exists(self.name) + conditions.append( + (IfNull(pr.applicable_for, "") != self.applicable_for) + | ( + (IfNull(pr.applicable_for, "") == self.applicable_for) + & IfNull(pr[applicable_for], "").notin(applicable_for_list) + ) + ) + else: + conditions.append(IfNull(pr.applicable_for, "") != "") - if docnames and not transaction_exists: - for docname in docnames: - frappe.delete_doc("Pricing Rule", docname.name) + return frappe.qb.from_(pr).select(pr.name).where(Criterion.all(conditions)).run(pluck=True) def on_update(self): self.validate() diff --git a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py index 74ba6cf923cb..a4ea81f0d9fb 100644 --- a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py @@ -90,6 +90,31 @@ def test_change_applicable_for_in_promotional_scheme(self): price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name}) self.assertEqual(price_rules, []) + def test_change_applicable_for_values_in_promotional_scheme(self): + ps = make_promotional_scheme(applicable_for="Customer", customer="_Test Customer") + ps.append("customer", {"customer": "_Test Customer 2"}) + ps.save() + + price_rules = frappe.get_all( + "Pricing Rule", filters={"promotional_scheme": ps.name, "applicable_for": "Customer"} + ) + self.assertTrue(len(price_rules), 2) + + ps.set("customer", []) + ps.append("customer", {"customer": "_Test Customer 2"}) + ps.save() + + price_rules = frappe.get_all( + "Pricing Rule", + filters={ + "promotional_scheme": ps.name, + "applicable_for": "Customer", + "customer": "_Test Customer", + }, + ) + self.assertEqual(price_rules, []) + frappe.delete_doc("Promotional Scheme", ps.name) + def test_min_max_amount_configuration(self): ps = make_promotional_scheme() ps.price_discount_slabs[0].min_amount = 10 diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 9c8c8cdfb030..be429689b5a1 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -31,6 +31,13 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. }, }; }); + + this.frm.set_query("expense_account", "items", function () { + return { + query: "erpnext.controllers.queries.get_expense_account", + filters: { company: doc.company }, + }; + }); } onload() { @@ -335,7 +342,9 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. party_type: "Supplier", account: this.frm.doc.credit_to, price_list: this.frm.doc.buying_price_list, - fetch_payment_terms_template: cint(!this.frm.doc.ignore_default_payment_terms_template), + fetch_payment_terms_template: cint( + (this.frm.doc.is_return == 0) & !this.frm.doc.ignore_default_payment_terms_template + ), }, function () { me.apply_pricing_rule(); @@ -506,13 +515,6 @@ cur_frm.fields_dict["select_print_heading"].get_query = function (doc, cdt, cdn) }; }; -cur_frm.set_query("expense_account", "items", function (doc) { - return { - query: "erpnext.controllers.queries.get_expense_account", - filters: { company: doc.company }, - }; -}); - cur_frm.set_query("wip_composite_asset", "items", function () { return { filters: { is_composite_asset: 1, docstatus: 0 }, @@ -561,11 +563,12 @@ frappe.ui.form.on("Purchase Invoice", { frm.custom_make_buttons = { "Purchase Invoice": "Return / Debit Note", "Payment Entry": "Payment", - "Landed Cost Voucher": function () { - frm.trigger("create_landed_cost_voucher"); - }, }; + if (frm.doc.update_stock) { + frm.custom_make_buttons["Landed Cost Voucher"] = "Landed Cost Voucher"; + } + frm.set_query("additional_discount_account", function () { return { filters: { @@ -607,20 +610,6 @@ frappe.ui.form.on("Purchase Invoice", { }); }, - create_landed_cost_voucher: function (frm) { - let lcv = frappe.model.get_new_doc("Landed Cost Voucher"); - lcv.company = frm.doc.company; - - let lcv_receipt = frappe.model.get_new_doc("Landed Cost Purchase Invoice"); - lcv_receipt.receipt_document_type = "Purchase Invoice"; - lcv_receipt.receipt_document = frm.doc.name; - lcv_receipt.supplier = frm.doc.supplier; - lcv_receipt.grand_total = frm.doc.grand_total; - lcv.purchase_receipts = [lcv_receipt]; - - frappe.set_route("Form", lcv.doctype, lcv.name); - }, - add_custom_buttons: function (frm) { if (frm.doc.docstatus == 1 && frm.doc.per_received < 100) { frm.add_custom_button( @@ -645,14 +634,40 @@ frappe.ui.form.on("Purchase Invoice", { __("View") ); } + + if (frm.doc.docstatus === 1 && frm.doc.update_stock) { + frm.add_custom_button( + __("Landed Cost Voucher"), + () => { + frm.events.make_lcv(frm); + }, + __("Create") + ); + } + }, + + make_lcv(frm) { + frappe.call({ + method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_lcv", + args: { + doctype: frm.doc.doctype, + docname: frm.doc.name, + }, + callback: (r) => { + if (r.message) { + var doc = frappe.model.sync(r.message); + frappe.set_route("Form", doc[0].doctype, doc[0].name); + } + }, + }); }, onload: function (frm) { - if (frm.doc.__onload && frm.is_new()) { - if (frm.doc.supplier) { + if (frm.doc.__onload && frm.doc.supplier) { + if (frm.is_new()) { frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0; } - if (!frm.doc.__onload.enable_apply_tds) { + if (!frm.doc.__onload.supplier_tds) { frm.set_df_property("apply_tds", "read_only", 1); } } diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 3751c027c975..6b21ec5b6787 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -1134,12 +1134,14 @@ "label": "Payment Terms" }, { + "depends_on": "eval:(!doc.is_paid && !doc.is_return)", "fieldname": "payment_terms_template", "fieldtype": "Link", "label": "Payment Terms Template", "options": "Payment Terms Template" }, { + "depends_on": "eval:(!doc.is_paid && !doc.is_return)", "fieldname": "payment_schedule", "fieldtype": "Table", "label": "Payment Schedule", @@ -1271,6 +1273,7 @@ "fieldtype": "Select", "in_standard_filter": 1, "label": "Status", + "no_copy": 1, "options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nPartly Paid\nUnpaid\nOverdue\nCancelled\nInternal Transfer", "print_hide": 1 }, @@ -1630,7 +1633,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2024-07-25 19:42:36.931278", + "modified": "2024-10-25 18:13:01.944477", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index c33b2c0ff17e..ebc4efc08a0e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -285,7 +285,6 @@ def validate(self): self.set_against_expense_account() self.validate_write_off_account() self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount") - self.create_remarks() self.set_status() self.validate_purchase_receipt_if_update_stock() validate_inter_company_party( @@ -322,10 +321,11 @@ def validate_cash(self): def create_remarks(self): if not self.remarks: - if self.bill_no and self.bill_date: - self.remarks = _("Against Supplier Invoice {0} dated {1}").format( - self.bill_no, formatdate(self.bill_date) - ) + if self.bill_no: + self.remarks = _("Against Supplier Invoice {0}").format(self.bill_no) + if self.bill_date: + self.remarks += " " + _("dated {0}").format(formatdate(self.bill_date)) + else: self.remarks = _("No Remarks") @@ -346,22 +346,6 @@ def set_missing_values(self, for_validate=False): self.tax_withholding_category = tds_category self.set_onload("supplier_tds", tds_category) - # If Linked Purchase Order has TDS applied, enable 'apply_tds' checkbox - if purchase_orders := [x.purchase_order for x in self.items if x.purchase_order]: - po = qb.DocType("Purchase Order") - po_with_tds = ( - qb.from_(po) - .select(po.name) - .where( - po.docstatus.eq(1) - & (po.name.isin(purchase_orders)) - & (po.apply_tds.eq(1)) - & (po.tax_withholding_category.notnull()) - ) - .run() - ) - self.set_onload("enable_apply_tds", True if po_with_tds else False) - super().set_missing_values(for_validate) def validate_credit_to_acc(self): @@ -377,16 +361,16 @@ def validate_credit_to_acc(self): if account.report_type != "Balance Sheet": frappe.throw( _( - "Please ensure {} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account." - ).format(frappe.bold("Credit To")), + "Please ensure that the {0} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account." + ).format(frappe.bold(_("Credit To"))), title=_("Invalid Account"), ) if self.supplier and account.account_type != "Payable": frappe.throw( _( - "Please ensure {} account {} is a Payable account. Change the account type to Payable or select a different account." - ).format(frappe.bold("Credit To"), frappe.bold(self.credit_to)), + "Please ensure that the {0} account {1} is a Payable account. You can change the account type to Payable or select a different account." + ).format(frappe.bold(_("Credit To")), frappe.bold(self.credit_to)), title=_("Invalid Account"), ) @@ -634,7 +618,7 @@ def po_required(self): "To submit the invoice without purchase order please set {0} as {1} in {2}" ).format( frappe.bold(_("Purchase Order Required")), - frappe.bold("No"), + frappe.bold(_("No")), get_link_to_form("Buying Settings", "Buying Settings", "Buying Settings"), ) throw(msg, title=_("Mandatory Purchase Order")) @@ -655,7 +639,7 @@ def pr_required(self): "To submit the invoice without purchase receipt please set {0} as {1} in {2}" ).format( frappe.bold(_("Purchase Receipt Required")), - frappe.bold("No"), + frappe.bold(_("No")), get_link_to_form("Buying Settings", "Buying Settings", "Buying Settings"), ) throw(msg, title=_("Mandatory Purchase Receipt")) @@ -747,6 +731,9 @@ def validate_for_repost(self): validate_docs_for_voucher_types(["Purchase Invoice"]) validate_docs_for_deferred_accounting([], [self.name]) + def before_submit(self): + self.create_remarks() + def on_submit(self): super().on_submit() @@ -876,6 +863,7 @@ def get_gl_entries(self, warehouse_account=None): self.make_tax_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries) + self.make_gl_entries_for_tax_withholding(gl_entries) gl_entries = make_regional_gl_entries(gl_entries, self) @@ -909,32 +897,37 @@ def make_supplier_gl_entry(self, gl_entries): ) if grand_total and not self.is_internal_transfer(): - against_voucher = self.name - if self.is_return and self.return_against and not self.update_outstanding_for_self: - against_voucher = self.return_against + self.add_supplier_gl_entry(gl_entries, base_grand_total, grand_total) + + def add_supplier_gl_entry( + self, gl_entries, base_grand_total, grand_total, against_account=None, remarks=None, skip_merge=False + ): + against_voucher = self.name + if self.is_return and self.return_against and not self.update_outstanding_for_self: + against_voucher = self.return_against + + # Did not use base_grand_total to book rounding loss gle + gl = { + "account": self.credit_to, + "party_type": "Supplier", + "party": self.supplier, + "due_date": self.due_date, + "against": against_account or self.against_expense_account, + "credit": base_grand_total, + "credit_in_account_currency": base_grand_total + if self.party_account_currency == self.company_currency + else grand_total, + "against_voucher": against_voucher, + "against_voucher_type": self.doctype, + "project": self.project, + "cost_center": self.cost_center, + "_skip_merge": skip_merge, + } - # Did not use base_grand_total to book rounding loss gle - gl_entries.append( - self.get_gl_dict( - { - "account": self.credit_to, - "party_type": "Supplier", - "party": self.supplier, - "due_date": self.due_date, - "against": self.against_expense_account, - "credit": base_grand_total, - "credit_in_account_currency": base_grand_total - if self.party_account_currency == self.company_currency - else grand_total, - "against_voucher": against_voucher, - "against_voucher_type": self.doctype, - "project": self.project, - "cost_center": self.cost_center, - }, - self.party_account_currency, - item=self, - ) - ) + if remarks: + gl["remarks"] = remarks + + gl_entries.append(self.get_gl_dict(gl, self.party_account_currency, item=self)) def make_item_gl_entries(self, gl_entries): # item gl entries @@ -1262,7 +1255,11 @@ def make_provisional_gl_entry(self, gl_entries, item): def update_gross_purchase_amount_for_linked_assets(self, item): assets = frappe.db.get_all( "Asset", - filters={"purchase_invoice": self.name, "item_code": item.item_code}, + filters={ + "purchase_invoice": self.name, + "item_code": item.item_code, + "purchase_invoice_item": ("in", [item.name, ""]), + }, fields=["name", "asset_quantity"], ) for asset in assets: @@ -1422,6 +1419,31 @@ def make_internal_transfer_gl_entries(self, gl_entries): ) ) + def make_gl_entries_for_tax_withholding(self, gl_entries): + """ + Tax withholding amount is not part of supplier invoice. + Separate supplier GL Entry for correct reporting. + """ + if not self.apply_tds: + return + + for row in self.get("taxes"): + if not row.is_tax_withholding_account or not row.tax_amount: + continue + + base_tds_amount = row.base_tax_amount_after_discount_amount + tds_amount = row.tax_amount_after_discount_amount + + self.add_supplier_gl_entry(gl_entries, base_tds_amount, tds_amount) + self.add_supplier_gl_entry( + gl_entries, + -base_tds_amount, + -tds_amount, + against_account=row.account_head, + remarks=_("TDS Deducted"), + skip_merge=True, + ) + def make_payment_gl_entries(self, gl_entries): # Make Cash GL Entries if cint(self.is_paid) and self.cash_bank_account and self.paid_amount: @@ -1515,10 +1537,29 @@ def make_gle_for_rounding_adjustment(self, gl_entries): # eg: rounding_adjustment = 0.01 and exchange rate = 0.05 and precision of base_rounding_adjustment is 2 # then base_rounding_adjustment becomes zero and error is thrown in GL Entry if not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment: - round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( + ( + round_off_account, + round_off_cost_center, + round_off_for_opening, + ) = get_round_off_account_and_cost_center( self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center ) + if self.is_opening == "Yes" and self.rounding_adjustment: + if not round_off_for_opening: + frappe.throw( + _( + "Opening Invoice has rounding adjustment of {0}.

'{1}' account is required to post these values. Please set it in Company: {2}.

Or, '{3}' can be enabled to not post any rounding adjustment." + ).format( + frappe.bold(self.rounding_adjustment), + frappe.bold("Round Off for Opening"), + get_link_to_form("Company", self.company), + frappe.bold("Disable Rounded Total"), + ) + ) + else: + round_off_account = round_off_for_opening + gl_entries.append( self.get_gl_dict( { @@ -1602,7 +1643,11 @@ def update_project(self): for proj, value in projects.items(): res = frappe.qb.from_(pj).select(pj.total_purchase_cost).where(pj.name == proj).for_update().run() current_purchase_cost = res and res[0][0] or 0 - frappe.db.set_value("Project", proj, "total_purchase_cost", current_purchase_cost + value) + # frappe.db.set_value("Project", proj, "total_purchase_cost", current_purchase_cost + value) + project_doc = frappe.get_doc("Project", proj) + project_doc.total_purchase_cost = current_purchase_cost + value + project_doc.calculate_gross_margin() + project_doc.db_update() def validate_supplier_invoice(self): if self.bill_date: diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index eb7c486f2e1b..e353435661c4 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1544,6 +1544,61 @@ def test_purchase_invoice_advance_taxes(self): payment_entry.load_from_db() self.assertEqual(payment_entry.taxes[0].allocated_amount, 0) + def test_purchase_gl_with_tax_withholding_tax(self): + company = "_Test Company" + + tds_account_args = { + "doctype": "Account", + "account_name": "TDS Payable", + "account_type": "Tax", + "parent_account": frappe.db.get_value( + "Account", {"account_name": "Duties and Taxes", "company": company} + ), + "company": company, + } + + tds_account = create_account(**tds_account_args) + tax_withholding_category = "Test TDS - 194 - Dividends - Individual" + + # Update tax withholding category with current fiscal year and rate details + create_tax_witholding_category(tax_withholding_category, company, tds_account) + + # create a new supplier to test + supplier = create_supplier( + supplier_name="_Test TDS Advance Supplier", + tax_withholding_category=tax_withholding_category, + ) + + pi = make_purchase_invoice( + supplier=supplier.name, + rate=3000, + qty=1, + item="_Test Non Stock Item", + do_not_submit=1, + ) + pi.apply_tds = 1 + pi.tax_withholding_category = tax_withholding_category + pi.save() + pi.submit() + + self.assertEqual(pi.taxes[0].tax_amount, 300) + self.assertEqual(pi.taxes[0].account_head, tds_account) + + gl_entries = frappe.get_all( + "GL Entry", + filters={"voucher_no": pi.name, "voucher_type": "Purchase Invoice", "account": "Creditors - _TC"}, + fields=["account", "against", "debit", "credit"], + ) + + for gle in gl_entries: + if gle.debit: + # GL Entry with TDS Amount + self.assertEqual(gle.against, tds_account) + self.assertEqual(gle.debit, 300) + else: + # GL Entry with Purchase Invoice Amount + self.assertEqual(gle.credit, 3000) + def test_provisional_accounting_entry(self): setup_provisional_accounting() @@ -1680,6 +1735,30 @@ def test_adjust_incoming_rate(self): frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1) + # Cost of Item is zero in Purchase Receipt + pr = make_purchase_receipt(qty=1, rate=0) + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + "stock_value_difference", + ) + self.assertEqual(stock_value_difference, 0) + + pi = create_purchase_invoice_from_receipt(pr.name) + for row in pi.items: + row.rate = 150 + + pi.save() + pi.submit() + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + "stock_value_difference", + ) + self.assertEqual(stock_value_difference, 150) + # Increase the cost of the item pr = make_purchase_receipt(qty=1, rate=100) @@ -2236,6 +2315,139 @@ def test_make_pr_and_pi_from_po(self): self.assertEqual(pi_expected_values[i][1], gle.debit) self.assertEqual(pi_expected_values[i][2], gle.credit) + def test_adjust_incoming_rate_from_pi_with_multi_currency(self): + from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import ( + make_landed_cost_voucher, + ) + + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0) + + frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1) + + # Increase the cost of the item + + pr = make_purchase_receipt( + qty=10, rate=1, currency="USD", do_not_save=1, supplier="_Test Supplier USD" + ) + pr.conversion_rate = 6300 + pr.plc_conversion_rate = 1 + pr.save() + pr.submit() + + self.assertEqual(pr.conversion_rate, 6300) + self.assertEqual(pr.plc_conversion_rate, 1) + self.assertEqual(pr.base_grand_total, 6300 * 10) + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + "stock_value_difference", + ) + self.assertEqual(stock_value_difference, 6300 * 10) + + make_landed_cost_voucher( + company=pr.company, + receipt_document_type="Purchase Receipt", + receipt_document=pr.name, + charges=3000, + distribute_charges_based_on="Qty", + ) + + pi = create_purchase_invoice_from_receipt(pr.name) + for row in pi.items: + row.rate = 1.1 + + pi.save() + pi.submit() + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + "stock_value_difference", + ) + self.assertEqual(stock_value_difference, 7230 * 10) + + frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0) + + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1) + + def test_last_purchase_rate(self): + item = create_item("_Test Item For Last Purchase Rate from PI", is_stock_item=1) + pi1 = make_purchase_invoice(item_code=item.item_code, qty=10, rate=100) + item.reload() + self.assertEqual(item.last_purchase_rate, 100) + + pi2 = make_purchase_invoice(item_code=item.item_code, qty=10, rate=200) + item.reload() + self.assertEqual(item.last_purchase_rate, 200) + + pi2.cancel() + item.reload() + self.assertEqual(item.last_purchase_rate, 100) + + pi1.cancel() + item.reload() + self.assertEqual(item.last_purchase_rate, 0) + + def test_opening_invoice_rounding_adjustment_validation(self): + pi = make_purchase_invoice(do_not_save=1) + pi.items[0].rate = 99.98 + pi.items[0].qty = 1 + pi.items[0].expense_account = "Temporary Opening - _TC" + pi.is_opening = "Yes" + pi.save() + self.assertRaises(frappe.ValidationError, pi.submit) + + def _create_opening_roundoff_account(self, company_name): + liability_root = frappe.db.get_all( + "Account", + filters={"company": company_name, "root_type": "Liability", "disabled": 0}, + order_by="lft", + limit=1, + )[0] + + # setup round off account + if acc := frappe.db.exists( + "Account", + { + "account_name": "Round Off for Opening", + "account_type": "Round Off for Opening", + "company": company_name, + }, + ): + frappe.db.set_value("Company", company_name, "round_off_for_opening", acc) + else: + acc = frappe.new_doc("Account") + acc.company = company_name + acc.parent_account = liability_root.name + acc.account_name = "Round Off for Opening" + acc.account_type = "Round Off for Opening" + acc.save() + frappe.db.set_value("Company", company_name, "round_off_for_opening", acc.name) + + def test_ledger_entries_of_opening_invoice_with_rounding_adjustment(self): + pi = make_purchase_invoice(do_not_save=1) + pi.items[0].rate = 99.98 + pi.items[0].qty = 1 + pi.items[0].expense_account = "Temporary Opening - _TC" + pi.is_opening = "Yes" + pi.save() + self._create_opening_roundoff_account(pi.company) + pi.submit() + actual = frappe.db.get_all( + "GL Entry", + filters={"voucher_no": pi.name, "is_opening": "Yes", "is_cancelled": False}, + fields=["account", "debit", "credit", "is_opening"], + order_by="account,debit", + ) + expected = [ + {"account": "Creditors - _TC", "debit": 0.0, "credit": 100.0, "is_opening": "Yes"}, + {"account": "Round Off for Opening - _TC", "debit": 0.02, "credit": 0.0, "is_opening": "Yes"}, + {"account": "Temporary Opening - _TC", "debit": 99.98, "credit": 0.0, "is_opening": "Yes"}, + ] + self.assertEqual(len(actual), 3) + self.assertEqual(expected, actual) + def set_advance_flag(company, flag, default_account): frappe.db.set_value( diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 8a2ba36cf620..258cc10d4ec4 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -505,7 +505,8 @@ "fieldtype": "Link", "label": "Project", "options": "Project", - "print_hide": 1 + "print_hide": 1, + "search_index": 1 }, { "allow_on_submit": 1, @@ -974,7 +975,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2024-07-19 12:12:42.449298", + "modified": "2024-10-28 15:06:19.246141", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py index 8c8ba633df02..0bd9a2a05154 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py @@ -45,9 +45,9 @@ def validate_for_closed_fiscal_year(self): latest_pcv = ( frappe.db.get_all( "Period Closing Voucher", - filters={"company": self.company}, - order_by="posting_date desc", - pluck="posting_date", + filters={"company": self.company, "docstatus": 1}, + order_by="period_end_date desc", + pluck="period_end_date", limit=1, ) or None diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py index f631ef437d61..9f906bb7647d 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py @@ -129,13 +129,15 @@ def test_04_pcv_validation(self): cost_center=self.cost_center, rate=100, ) + fy = get_fiscal_year(today(), company=self.company) pcv = frappe.get_doc( { "doctype": "Period Closing Voucher", "transaction_date": today(), - "posting_date": today(), + "period_start_date": fy[1], + "period_end_date": today(), "company": self.company, - "fiscal_year": get_fiscal_year(today(), company=self.company)[0], + "fiscal_year": fy[0], "cost_center": self.cost_center, "closing_account_head": self.retained_earnings, "remarks": "test", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 87b71cd793f1..75c71ef6eb39 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -339,6 +339,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( account: this.frm.doc.debit_to, price_list: this.frm.doc.selling_price_list, pos_profile: pos_profile, + fetch_payment_terms_template: cint( + (this.frm.doc.is_return == 0) & !this.frm.doc.ignore_default_payment_terms_template + ), }, function () { me.apply_pricing_rule(); @@ -738,20 +741,6 @@ frappe.ui.form.on("Sales Invoice", { }; }; - frm.set_query("company_address", function (doc) { - if (!doc.company) { - frappe.throw(__("Please set Company")); - } - - return { - query: "frappe.contacts.doctype.address.address.address_query", - filters: { - link_doctype: "Company", - link_name: doc.company, - }, - }; - }); - frm.set_query("pos_profile", function (doc) { if (!doc.company) { frappe.throw(__("Please set Company")); diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index c44afd555e05..cb861e68cdc7 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -159,8 +159,9 @@ "dispatch_address", "company_address_section", "company_address", - "company_addr_col_break", "company_address_display", + "company_addr_col_break", + "company_contact_person", "terms_tab", "payment_schedule_section", "ignore_default_payment_terms_template", @@ -2166,6 +2167,13 @@ "label": "Update Outstanding for Self", "no_copy": 1, "print_hide": 1 + }, + { + "fieldname": "company_contact_person", + "fieldtype": "Link", + "label": "Company Contact Person", + "options": "Contact", + "print_hide": 1 } ], "icon": "fa fa-file-text", @@ -2178,7 +2186,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2024-07-18 15:30:39.428519", + "modified": "2024-11-26 12:34:09.110690", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", @@ -2233,4 +2241,4 @@ "title_field": "title", "track_changes": 1, "track_seen": 1 -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 5d6c8a353fba..1a7ffc3c339b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -96,6 +96,7 @@ class SalesInvoice(SellingController): company: DF.Link company_address: DF.Link | None company_address_display: DF.SmallText | None + company_contact_person: DF.Link | None company_tax_id: DF.Data | None contact_display: DF.SmallText | None contact_email: DF.Data | None @@ -278,7 +279,6 @@ def validate(self): self.check_sales_order_on_hold_or_close("sales_order") self.validate_debit_to_acc() self.clear_unallocated_advances("Sales Invoice Advance", "advances") - self.add_remarks() self.validate_fixed_asset() self.set_income_account_for_fixed_assets() self.validate_item_cost_centers() @@ -298,8 +298,11 @@ def validate(self): self.update_current_stock() self.validate_delivery_note() + is_deferred_invoice = any(d.get("enable_deferred_revenue") for d in self.get("items")) + # validate service stop date to lie in between start and end date - validate_service_stop_date(self) + if is_deferred_invoice: + validate_service_stop_date(self) if not self.is_opening: self.is_opening = "No" @@ -341,6 +344,7 @@ def validate(self): ): validate_loyalty_points(self, self.loyalty_points) + self.allow_write_off_only_on_pos() self.reset_default_field_value("set_warehouse", "items", "warehouse") def validate_accounts(self): @@ -422,6 +426,9 @@ def before_save(self): self.set_account_for_mode_of_payment() self.set_paid_amount() + def before_submit(self): + self.add_remarks() + def on_submit(self): self.validate_pos_paid_amount() @@ -514,7 +521,7 @@ def check_if_consolidated_invoice(self): ) if pos_closing_entry and pos_closing_entry[0]: msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format( - frappe.bold("Consolidated Sales Invoice"), + frappe.bold(_("Consolidated Sales Invoice")), get_link_to_form("POS Closing Entry", pos_closing_entry[0]), ) frappe.throw(msg, title=_("Not Allowed")) @@ -858,7 +865,7 @@ def validate_debit_to_acc(self): if account.report_type != "Balance Sheet": msg = ( - _("Please ensure {} account is a Balance Sheet account.").format(frappe.bold("Debit To")) + _("Please ensure {} account is a Balance Sheet account.").format(frappe.bold(_("Debit To"))) + " " ) msg += _( @@ -869,7 +876,7 @@ def validate_debit_to_acc(self): if self.customer and account.account_type != "Receivable": msg = ( _("Please ensure {} account {} is a Receivable account.").format( - frappe.bold("Debit To"), frappe.bold(self.debit_to) + frappe.bold(_("Debit To")), frappe.bold(self.debit_to) ) + " " ) @@ -946,10 +953,11 @@ def force_set_against_income_account(self): def add_remarks(self): if not self.remarks: - if self.po_no and self.po_date: - self.remarks = _("Against Customer Order {0} dated {1}").format( - self.po_no, formatdate(self.po_date) - ) + if self.po_no: + self.remarks = _("Against Customer Order {0}").format(self.po_no) + if self.po_date: + self.remarks += " " + _("dated {0}").format(formatdate(self.po_date)) + else: self.remarks = _("No Remarks") @@ -1018,6 +1026,10 @@ def validate_delivery_note(self): raise_exception=1, ) + def allow_write_off_only_on_pos(self): + if not self.is_pos and self.write_off_account: + self.write_off_account = None + def validate_write_off_account(self): if flt(self.write_off_amount) and not self.write_off_account: self.write_off_account = frappe.get_cached_value("Company", self.company, "write_off_account") @@ -1351,14 +1363,15 @@ def make_item_gl_entries(self, gl_entries): else: if asset.calculate_depreciation: - notes = _( - "This schedule was created when Asset {0} was sold through Sales Invoice {1}." - ).format( - get_link_to_form(asset.doctype, asset.name), - get_link_to_form(self.doctype, self.get("name")), - ) - depreciate_asset(asset, self.posting_date, notes) - asset.reload() + if not asset.status == "Fully Depreciated": + notes = _( + "This schedule was created when Asset {0} was sold through Sales Invoice {1}." + ).format( + get_link_to_form(asset.doctype, asset.name), + get_link_to_form(self.doctype, self.get("name")), + ) + depreciate_asset(asset, self.posting_date, notes) + asset.reload() fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( asset, @@ -1621,10 +1634,29 @@ def make_gle_for_rounding_adjustment(self, gl_entries): and self.base_rounding_adjustment and not self.is_internal_transfer() ): - round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( + ( + round_off_account, + round_off_cost_center, + round_off_for_opening, + ) = get_round_off_account_and_cost_center( self.company, "Sales Invoice", self.name, self.use_company_roundoff_cost_center ) + if self.is_opening == "Yes" and self.rounding_adjustment: + if not round_off_for_opening: + frappe.throw( + _( + "Opening Invoice has rounding adjustment of {0}.

'{1}' account is required to post these values. Please set it in Company: {2}.

Or, '{3}' can be enabled to not post any rounding adjustment." + ).format( + frappe.bold(self.rounding_adjustment), + frappe.bold("Round Off for Opening"), + get_link_to_form("Company", self.company), + frappe.bold("Disable Rounded Total"), + ) + ) + else: + round_off_account = round_off_for_opening + gl_entries.append( self.get_gl_dict( { @@ -1722,9 +1754,14 @@ def validate_serial_against_delivery_note(self): ) def update_project(self): - if self.project: - project = frappe.get_doc("Project", self.project) + unique_projects = list(set([d.project for d in self.get("items") if d.project])) + if self.project and self.project not in unique_projects: + unique_projects.append(self.project) + + for p in unique_projects: + project = frappe.get_doc("Project", p) project.update_billed_amount() + project.calculate_gross_margin() project.db_update() def verify_payment_amount_is_positive(self): @@ -2115,7 +2152,7 @@ def update_item(source_doc, target_doc, source_parent): "postprocess": update_item, "condition": lambda doc: doc.delivered_by_supplier != 1, }, - "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, + "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True}, "Sales Team": { "doctype": "Sales Team", "field_map": {"incentives": "incentives"}, diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js index f971f68a4546..3371a63cca28 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js @@ -15,7 +15,7 @@ frappe.listview_settings["Sales Invoice"] = { ], get_indicator: function (doc) { const status_colors = { - Draft: "grey", + Draft: "red", Unpaid: "orange", Paid: "green", Return: "gray", diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index fef30bdfecd0..57eb84caaa49 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -8,7 +8,7 @@ from frappe import qb from frappe.model.dynamic_links import get_dynamic_link_map from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import add_days, flt, getdate, nowdate, today +from frappe.utils import add_days, flt, format_date, getdate, nowdate, today import erpnext from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account @@ -314,7 +314,8 @@ def test_sales_invoice_with_discount_and_inclusive_tax(self): si.insert() # with inclusive tax - self.assertEqual(si.items[0].net_amount, 3947.368421052631) + self.assertEqual(si.items[0].net_amount, 3947.37) + self.assertEqual(si.net_total, si.base_net_total) self.assertEqual(si.net_total, 3947.37) self.assertEqual(si.grand_total, 5000) @@ -658,7 +659,7 @@ def test_sales_invoice_calculation_base_currency_with_tax_inclusive_price(self): 62.5, 625.0, 50, - 499.97600115194473, + 499.98, ], "_Test Item Home Desktop 200": [ 190.66, @@ -669,7 +670,7 @@ def test_sales_invoice_calculation_base_currency_with_tax_inclusive_price(self): 190.66, 953.3, 150, - 749.9968530500239, + 750, ], } @@ -682,20 +683,21 @@ def test_sales_invoice_calculation_base_currency_with_tax_inclusive_price(self): self.assertEqual(d.get(k), expected_values[d.item_code][i]) # check net total - self.assertEqual(si.net_total, 1249.97) + self.assertEqual(si.base_net_total, si.net_total) + self.assertEqual(si.net_total, 1249.98) self.assertEqual(si.total, 1578.3) # check tax calculation expected_values = { "keys": ["tax_amount", "total"], - "_Test Account Excise Duty - _TC": [140, 1389.97], - "_Test Account Education Cess - _TC": [2.8, 1392.77], - "_Test Account S&H Education Cess - _TC": [1.4, 1394.17], - "_Test Account CST - _TC": [27.88, 1422.05], - "_Test Account VAT - _TC": [156.25, 1578.30], - "_Test Account Customs Duty - _TC": [125, 1703.30], - "_Test Account Shipping Charges - _TC": [100, 1803.30], - "_Test Account Discount - _TC": [-180.33, 1622.97], + "_Test Account Excise Duty - _TC": [140, 1389.98], + "_Test Account Education Cess - _TC": [2.8, 1392.78], + "_Test Account S&H Education Cess - _TC": [1.4, 1394.18], + "_Test Account CST - _TC": [27.88, 1422.06], + "_Test Account VAT - _TC": [156.25, 1578.31], + "_Test Account Customs Duty - _TC": [125, 1703.31], + "_Test Account Shipping Charges - _TC": [100, 1803.31], + "_Test Account Discount - _TC": [-180.33, 1622.98], } for d in si.get("taxes"): @@ -731,7 +733,7 @@ def test_sales_invoice_calculation_export_currency_with_tax_inclusive_price(self "base_rate": 2500, "base_amount": 25000, "net_rate": 40, - "net_amount": 399.9808009215558, + "net_amount": 399.98, "base_net_rate": 2000, "base_net_amount": 19999, }, @@ -745,7 +747,7 @@ def test_sales_invoice_calculation_export_currency_with_tax_inclusive_price(self "base_rate": 7500, "base_amount": 37500, "net_rate": 118.01, - "net_amount": 590.0531205155963, + "net_amount": 590.05, "base_net_rate": 5900.5, "base_net_amount": 29502.5, }, @@ -783,8 +785,13 @@ def test_sales_invoice_calculation_export_currency_with_tax_inclusive_price(self self.assertEqual(si.base_grand_total, 60795) self.assertEqual(si.grand_total, 1215.90) - self.assertEqual(si.rounding_adjustment, 0.01) - self.assertEqual(si.base_rounding_adjustment, 0.50) + # no rounding adjustment as the Smallest Currency Fraction Value of USD is 0.01 + if frappe.db.get_value("Currency", "USD", "smallest_currency_fraction_value") < 0.01: + self.assertEqual(si.rounding_adjustment, 0.10) + self.assertEqual(si.base_rounding_adjustment, 5.0) + else: + self.assertEqual(si.rounding_adjustment, 0.0) + self.assertEqual(si.base_rounding_adjustment, 0.0) def test_outstanding(self): w = self.make() @@ -1995,7 +2002,7 @@ def test_outstanding_amount_after_advance_payment_entry_cancellation(self): # Check if SO is unlinked/replaced by SI in PE & if SO advance paid is 0 self.assertEqual(pe.references[0].reference_name, si.name) - self.assertEqual(sales_order.advance_paid, 0.0) + self.assertEqual(sales_order.advance_paid, 300.0) # check outstanding after advance allocation self.assertEqual( @@ -2172,7 +2179,7 @@ def test_rounding_adjustment(self): def test_rounding_adjustment_2(self): si = create_sales_invoice(rate=400, do_not_save=True) - for rate in [400, 600, 100]: + for rate in [400.25, 600.30, 100.65]: si.append( "items", { @@ -2198,18 +2205,19 @@ def test_rounding_adjustment_2(self): ) si.save() si.submit() - self.assertEqual(si.net_total, 1271.19) - self.assertEqual(si.grand_total, 1500) - self.assertEqual(si.total_taxes_and_charges, 228.82) - self.assertEqual(si.rounding_adjustment, -0.01) + self.assertEqual(si.net_total, si.base_net_total) + self.assertEqual(si.net_total, 1272.20) + self.assertEqual(si.grand_total, 1501.20) + self.assertEqual(si.total_taxes_and_charges, 229) + self.assertEqual(si.rounding_adjustment, -0.20) round_off_account = frappe.get_cached_value("Company", "_Test Company", "round_off_account") expected_values = { - "_Test Account Service Tax - _TC": [0.0, 114.41], - "_Test Account VAT - _TC": [0.0, 114.41], - si.debit_to: [1500, 0.0], - round_off_account: [0.01, 0.01], - "Sales - _TC": [0.0, 1271.18], + "_Test Account Service Tax - _TC": [0.0, 114.50], + "_Test Account VAT - _TC": [0.0, 114.50], + si.debit_to: [1501, 0.0], + round_off_account: [0.20, 0.0], + "Sales - _TC": [0.0, 1272.20], } gl_entries = frappe.db.sql( @@ -2267,7 +2275,8 @@ def test_rounding_adjustment_3(self): si.save() si.submit() - self.assertEqual(si.net_total, 4007.16) + self.assertEqual(si.net_total, si.base_net_total) + self.assertEqual(si.net_total, 4007.15) self.assertEqual(si.grand_total, 4488.02) self.assertEqual(si.total_taxes_and_charges, 480.86) self.assertEqual(si.rounding_adjustment, -0.02) @@ -2280,7 +2289,7 @@ def test_rounding_adjustment_3(self): ["_Test Account Service Tax - _TC", 0.0, 240.43], ["_Test Account VAT - _TC", 0.0, 240.43], ["Sales - _TC", 0.0, 4007.15], - [round_off_account, 0.02, 0.01], + [round_off_account, 0.01, 0.0], ] ) @@ -3162,6 +3171,50 @@ def test_sales_invoice_against_supplier_usd_with_dimensions(self): party_link.delete() frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0) + def test_sales_invoice_cancel_with_common_party_advance_jv(self): + from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import ( + make_customer, + ) + from erpnext.accounts.doctype.party_link.party_link import create_party_link + from erpnext.buying.doctype.supplier.test_supplier import create_supplier + + # create a customer + customer = make_customer(customer="_Test Common Supplier") + # create a supplier + supplier = create_supplier(supplier_name="_Test Common Supplier").name + + # create a party link between customer & supplier + party_link = create_party_link("Supplier", supplier, customer) + + # enable common party accounting + frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 1) + + # create a sales invoice + si = create_sales_invoice(customer=customer) + + # check creation of journal entry + jv = frappe.db.get_value( + "Journal Entry Account", + filters={ + "reference_type": si.doctype, + "reference_name": si.name, + "docstatus": 1, + }, + fieldname="parent", + ) + + self.assertTrue(jv) + + # cancel sales invoice + si.cancel() + + # check cancellation of journal entry + jv_status = frappe.db.get_value("Journal Entry", jv, "docstatus") + self.assertEqual(jv_status, 2) + + party_link.delete() + frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0) + def test_payment_statuses(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry @@ -3871,6 +3924,313 @@ def test_pos_returns_without_update_outstanding_for_self(self): self.assertEqual(len(res), 1) self.assertEqual(res[0][0], pos_return.return_against) + @change_settings("Accounts Settings", {"enable_common_party_accounting": True}) + def test_common_party_with_foreign_currency_jv(self): + from erpnext.accounts.doctype.account.test_account import create_account + from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import ( + make_customer, + ) + from erpnext.accounts.doctype.party_link.party_link import create_party_link + from erpnext.buying.doctype.supplier.test_supplier import create_supplier + from erpnext.setup.utils import get_exchange_rate + + creditors = create_account( + account_name="Creditors USD", + parent_account="Accounts Payable - _TC", + company="_Test Company", + account_currency="USD", + account_type="Payable", + ) + debtors = create_account( + account_name="Debtors USD", + parent_account="Accounts Receivable - _TC", + company="_Test Company", + account_currency="USD", + account_type="Receivable", + ) + + # create a customer + customer = make_customer(customer="_Test Common Party USD") + cust_doc = frappe.get_doc("Customer", customer) + cust_doc.default_currency = "USD" + test_account_details = { + "company": "_Test Company", + "account": debtors, + } + cust_doc.append("accounts", test_account_details) + cust_doc.save() + + # create a supplier + supplier = create_supplier(supplier_name="_Test Common Party USD").name + supp_doc = frappe.get_doc("Supplier", supplier) + supp_doc.default_currency = "USD" + test_account_details = { + "company": "_Test Company", + "account": creditors, + } + supp_doc.append("accounts", test_account_details) + supp_doc.save() + + # create a party link between customer & supplier + create_party_link("Supplier", supplier, customer) + + # create a sales invoice + si = create_sales_invoice( + customer=customer, + currency="USD", + conversion_rate=get_exchange_rate("USD", "INR"), + debit_to=debtors, + do_not_save=1, + ) + si.party_account_currency = "USD" + si.save() + si.submit() + + # check outstanding of sales invoice + si.reload() + self.assertEqual(si.status, "Paid") + self.assertEqual(flt(si.outstanding_amount), 0.0) + + # check creation of journal entry + jv = frappe.get_all( + "Journal Entry Account", + { + "account": si.debit_to, + "party_type": "Customer", + "party": si.customer, + "reference_type": si.doctype, + "reference_name": si.name, + }, + pluck="credit_in_account_currency", + ) + self.assertTrue(jv) + self.assertEqual(jv[0], si.grand_total) + + def test_invoice_remarks(self): + si = frappe.copy_doc(test_records[0]) + si.po_no = "Test PO" + si.po_date = nowdate() + si.save() + si.submit() + self.assertEqual(si.remarks, f"Against Customer Order Test PO dated {format_date(nowdate())}") + + def test_gl_voucher_subtype(self): + si = create_sales_invoice() + gl_entries = frappe.get_all( + "GL Entry", + filters={"voucher_type": "Sales Invoice", "voucher_no": si.name}, + pluck="voucher_subtype", + ) + + self.assertTrue(all([x == "Sales Invoice" for x in gl_entries])) + + si = create_sales_invoice(is_return=1, qty=-1) + gl_entries = frappe.get_all( + "GL Entry", + filters={"voucher_type": "Sales Invoice", "voucher_no": si.name}, + pluck="voucher_subtype", + ) + + self.assertTrue(all([x == "Credit Note" for x in gl_entries])) + + def test_validation_on_opening_invoice_with_rounding(self): + si = create_sales_invoice(qty=1, rate=99.98, do_not_submit=True) + si.is_opening = "Yes" + si.items[0].income_account = "Temporary Opening - _TC" + si.save() + self.assertRaises(frappe.ValidationError, si.submit) + + def _create_opening_roundoff_account(self, company_name): + liability_root = frappe.db.get_all( + "Account", + filters={"company": company_name, "root_type": "Liability", "disabled": 0}, + order_by="lft", + limit=1, + )[0] + + # setup round off account + if acc := frappe.db.exists( + "Account", + { + "account_name": "Round Off for Opening", + "account_type": "Round Off for Opening", + "company": company_name, + }, + ): + frappe.db.set_value("Company", company_name, "round_off_for_opening", acc) + else: + acc = frappe.new_doc("Account") + acc.company = company_name + acc.parent_account = liability_root.name + acc.account_name = "Round Off for Opening" + acc.account_type = "Round Off for Opening" + acc.save() + frappe.db.set_value("Company", company_name, "round_off_for_opening", acc.name) + + def test_opening_invoice_with_rounding_adjustment(self): + si = create_sales_invoice(qty=1, rate=99.98, do_not_submit=True) + si.is_opening = "Yes" + si.items[0].income_account = "Temporary Opening - _TC" + si.save() + + self._create_opening_roundoff_account(si.company) + + si.reload() + si.submit() + res = frappe.db.get_all( + "GL Entry", + filters={"voucher_no": si.name, "is_opening": "Yes"}, + fields=["account", "debit", "credit", "is_opening"], + ) + self.assertEqual(len(res), 3) + + def _create_opening_invoice_with_inclusive_tax(self): + si = create_sales_invoice(qty=1, rate=90, do_not_submit=True) + si.is_opening = "Yes" + si.items[0].income_account = "Temporary Opening - _TC" + item_template = si.items[0].as_dict() + item_template.name = None + item_template.rate = 55 + si.append("items", item_template) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Testing...", + "rate": 5, + "included_in_print_rate": True, + }, + ) + # there will be 0.01 precision loss between Dr and Cr + # caused by 'included_in_print_tax' option + si.save() + return si + + def test_rounding_validation_for_opening_with_inclusive_tax(self): + si = self._create_opening_invoice_with_inclusive_tax() + # 'Round Off for Opening' not set in Company master + # Ledger level validation must be thrown + self.assertRaises(frappe.ValidationError, si.submit) + + def test_ledger_entries_on_opening_invoice_with_rounding_loss_by_inclusive_tax(self): + si = self._create_opening_invoice_with_inclusive_tax() + # 'Round Off for Opening' is set in Company master + self._create_opening_roundoff_account(si.company) + + si.submit() + actual = frappe.db.get_all( + "GL Entry", + filters={"voucher_no": si.name, "is_opening": "Yes", "is_cancelled": False}, + fields=["account", "debit", "credit", "is_opening"], + order_by="account,debit", + ) + expected = [ + {"account": "_Test Account Service Tax - _TC", "debit": 0.0, "credit": 6.9, "is_opening": "Yes"}, + {"account": "Debtors - _TC", "debit": 145.0, "credit": 0.0, "is_opening": "Yes"}, + {"account": "Round Off for Opening - _TC", "debit": 0.0, "credit": 0.01, "is_opening": "Yes"}, + {"account": "Temporary Opening - _TC", "debit": 0.0, "credit": 138.09, "is_opening": "Yes"}, + ] + self.assertEqual(len(actual), 4) + self.assertEqual(expected, actual) + + @change_settings("Accounts Settings", {"enable_common_party_accounting": True}) + def test_common_party_with_different_currency_in_debtor_and_creditor(self): + from erpnext.accounts.doctype.account.test_account import create_account + from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import ( + make_customer, + ) + from erpnext.accounts.doctype.party_link.party_link import create_party_link + from erpnext.buying.doctype.supplier.test_supplier import create_supplier + from erpnext.setup.utils import get_exchange_rate + + creditors = create_account( + account_name="Creditors INR", + parent_account="Accounts Payable - _TC", + company="_Test Company", + account_currency="INR", + account_type="Payable", + ) + debtors = create_account( + account_name="Debtors USD", + parent_account="Accounts Receivable - _TC", + company="_Test Company", + account_currency="USD", + account_type="Receivable", + ) + + # create a customer + customer = make_customer(customer="_Test Common Party USD") + cust_doc = frappe.get_doc("Customer", customer) + cust_doc.default_currency = "USD" + test_account_details = { + "company": "_Test Company", + "account": debtors, + } + cust_doc.append("accounts", test_account_details) + cust_doc.save() + + # create a supplier + supplier = create_supplier(supplier_name="_Test Common Party INR").name + supp_doc = frappe.get_doc("Supplier", supplier) + supp_doc.default_currency = "INR" + test_account_details = { + "company": "_Test Company", + "account": creditors, + } + supp_doc.append("accounts", test_account_details) + supp_doc.save() + + # create a party link between customer & supplier + create_party_link("Supplier", supplier, customer) + + # create a sales invoice + si = create_sales_invoice( + customer=customer, + currency="USD", + conversion_rate=get_exchange_rate("USD", "INR"), + debit_to=debtors, + do_not_save=1, + ) + si.party_account_currency = "USD" + si.save() + si.submit() + + # check outstanding of sales invoice + si.reload() + self.assertEqual(si.status, "Paid") + self.assertEqual(flt(si.outstanding_amount), 0.0) + + # check creation of journal entry + jv = frappe.get_all( + "Journal Entry Account", + { + "account": si.debit_to, + "party_type": "Customer", + "party": si.customer, + "reference_type": si.doctype, + "reference_name": si.name, + }, + pluck="credit_in_account_currency", + ) + self.assertTrue(jv) + self.assertEqual(jv[0], si.grand_total) + + def test_total_billed_amount(self): + si = create_sales_invoice(do_not_submit=True) + + project = frappe.new_doc("Project") + project.project_name = "Test Total Billed Amount" + project.save() + + si.project = project.name + si.save() + si.submit() + + doc = frappe.get_doc("Project", project.name) + self.assertEqual(doc.total_billed_amount, si.grand_total) + def set_advance_flag(company, flag, default_account): frappe.db.set_value( diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index 932bc8e49d4d..dd370ef414f1 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -89,11 +89,14 @@ "incoming_rate", "item_tax_rate", "actual_batch_qty", - "actual_qty", "section_break_eoec", "serial_no", "column_break_ytgd", "batch_no", + "available_quantity_section", + "actual_qty", + "column_break_ogff", + "company_total_stock", "edit_references", "sales_order", "so_detail", @@ -675,7 +678,8 @@ "allow_on_submit": 1, "fieldname": "actual_qty", "fieldtype": "Float", - "label": "Available Qty at Warehouse", + "label": "Qty (Warehouse)", + "no_copy": 1, "oldfieldname": "actual_qty", "oldfieldtype": "Currency", "print_hide": 1, @@ -812,7 +816,8 @@ "fieldname": "project", "fieldtype": "Link", "label": "Project", - "options": "Project" + "options": "Project", + "search_index": 1 }, { "depends_on": "eval:parent.update_stock == 1", @@ -922,12 +927,30 @@ { "fieldname": "column_break_ytgd", "fieldtype": "Column Break" + }, + { + "fieldname": "available_quantity_section", + "fieldtype": "Section Break", + "label": "Available Quantity" + }, + { + "fieldname": "column_break_ogff", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "fieldname": "company_total_stock", + "fieldtype": "Float", + "label": "Qty (Company)", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2024-05-23 16:36:18.970862", + "modified": "2024-11-25 16:27:33.287341", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py index 9be1b42aab3e..b7b0873c76b7 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py @@ -28,6 +28,7 @@ class SalesInvoiceItem(Document): base_rate_with_margin: DF.Currency batch_no: DF.Link | None brand: DF.Data | None + company_total_stock: DF.Float conversion_factor: DF.Float cost_center: DF.Link customer_item_code: DF.Data | None diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 916757a8d6d7..d57f1de43794 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -737,10 +737,7 @@ def force_fetch_subscription_updates(self): elif self.generate_invoice_at == "Days before the current subscription period": processing_date = add_days(self.current_invoice_start, -self.number_of_days) - process_subscription = frappe.new_doc("Process Subscription") - process_subscription.posting_date = processing_date - process_subscription.subscription = self.name - process_subscription.save().submit() + self.process(posting_date=processing_date) def is_prorate() -> int: diff --git a/erpnext/accounts/doctype/tax_rule/tax_rule.py b/erpnext/accounts/doctype/tax_rule/tax_rule.py index ed623c6635e6..1c0c0a3c1d61 100644 --- a/erpnext/accounts/doctype/tax_rule/tax_rule.py +++ b/erpnext/accounts/doctype/tax_rule/tax_rule.py @@ -185,7 +185,7 @@ def get_tax_template(posting_date, args): conditions.append("(from_date is null) and (to_date is null)") conditions.append( - "ifnull(tax_category, '') = {}".format(frappe.db.escape(cstr(args.get("tax_category")))) + "ifnull(tax_category, '') = {}".format(frappe.db.escape(cstr(args.get("tax_category")), False)) ) if "tax_category" in args.keys(): del args["tax_category"] diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 9fd78da2d072..69c7eb1153c5 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -327,7 +327,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N tax_amount = 0 else: # if no TCS has been charged in FY, - # then chargeable value is "prev invoices + advances" value which cross the threshold + # then chargeable value is "prev invoices + advances - advance_adjusted" value which cross the threshold tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers) if cint(tax_details.round_off_tax_amount): @@ -414,6 +414,9 @@ def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, pa Use Payment Ledger to fetch unallocated Advance Payments """ + if party_type == "Supplier": + return [] + ple = qb.DocType("Payment Ledger Entry") conditions = [] @@ -511,7 +514,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers): payment_entry_filters.pop("apply_tax_withholding_amount", None) payment_entry_filters.pop("tax_withholding_category", None) - supp_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0 + supp_inv_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0 supp_jv_credit_amt = ( frappe.db.get_value( @@ -535,7 +538,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers): group_by="payment_type", ) - supp_credit_amt += supp_jv_credit_amt + supp_credit_amt = supp_jv_credit_amt supp_credit_amt += inv.tax_withholding_net_total for type in payment_entry_amounts: @@ -553,19 +556,19 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers): tax_withholding_net_total = inv.tax_withholding_net_total if (threshold and tax_withholding_net_total >= threshold) or ( - cumulative_threshold and supp_credit_amt >= cumulative_threshold + cumulative_threshold and (supp_credit_amt + supp_inv_credit_amt) >= cumulative_threshold ): + # Get net total again as TDS is calculated on net total + # Grand is used to just check for threshold breach + net_total = ( + frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(tax_withholding_net_total)") or 0.0 + ) + supp_credit_amt += net_total + if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint( tax_details.tax_on_excess_amount ): - # Get net total again as TDS is calculated on net total - # Grand is used to just check for threshold breach - net_total = ( - frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(tax_withholding_net_total)") - or 0.0 - ) - net_total += inv.tax_withholding_net_total - supp_credit_amt = net_total - cumulative_threshold + supp_credit_amt = net_total + tax_withholding_net_total - cumulative_threshold if ldc and is_valid_certificate(ldc, inv.get("posting_date") or inv.get("transaction_date"), 0): tds_amount = get_lower_deduction_amount( @@ -607,8 +610,6 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers): conditions.append(ple.voucher_no == ple.against_voucher_no) conditions.append(ple.company == inv.company) - (qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run(as_list=1)) - advance_amt = ( qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run()[0][0] or 0.0 ) @@ -631,9 +632,12 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers): ) cumulative_threshold = tax_details.get("cumulative_threshold", 0) + advance_adjusted = get_advance_adjusted_in_invoice(inv) current_invoice_total = get_invoice_total_without_tcs(inv, tax_details) - total_invoiced_amt = current_invoice_total + invoiced_amt + advance_amt - credit_note_amt + total_invoiced_amt = ( + current_invoice_total + invoiced_amt + advance_amt - credit_note_amt - advance_adjusted + ) if cumulative_threshold and total_invoiced_amt >= cumulative_threshold: chargeable_amt = total_invoiced_amt - cumulative_threshold @@ -642,6 +646,14 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers): return tcs_amount +def get_advance_adjusted_in_invoice(inv): + advances_adjusted = 0 + for row in inv.get("advances", []): + advances_adjusted += row.allocated_amount + + return advances_adjusted + + def get_invoice_total_without_tcs(inv, tax_details): tcs_tax_row = [d for d in inv.taxes if d.account_head == tax_details.account_head] tcs_tax_row_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0 diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index 1e3939d98a44..f9f34380d55f 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -74,11 +74,17 @@ def test_single_threshold_tds(self): self.assertEqual(pi.grand_total, 18000) # check gl entry for the purchase invoice - gl_entries = frappe.db.get_all("GL Entry", filters={"voucher_no": pi.name}, fields=["*"]) + gl_entries = frappe.db.get_all( + "GL Entry", + filters={"voucher_no": pi.name}, + fields=["account", "sum(debit) as debit", "sum(credit) as credit"], + group_by="account", + ) self.assertEqual(len(gl_entries), 3) for d in gl_entries: if d.account == pi.credit_to: - self.assertEqual(d.credit, 18000) + self.assertEqual(d.credit, 20000) + self.assertEqual(d.debit, 2000) elif d.account == pi.items[0].get("expense_account"): self.assertEqual(d.debit, 20000) elif d.account == pi.taxes[0].get("account_head"): @@ -121,6 +127,85 @@ def test_tax_withholding_category_checks(self): for d in reversed(invoices): d.cancel() + def test_cumulative_threshold_with_party_ledger_amount_on_net_total(self): + invoices = [] + frappe.db.set_value( + "Supplier", "Test TDS Supplier3", "tax_withholding_category", "Advance TDS Category" + ) + + # Invoice with tax and without exceeding single and cumulative thresholds + for _ in range(2): + pi = create_purchase_invoice(supplier="Test TDS Supplier3", rate=1000, do_not_save=True) + pi.apply_tds = 1 + pi.append( + "taxes", + { + "category": "Total", + "charge_type": "Actual", + "account_head": "_Test Account VAT - _TC", + "cost_center": "Main - _TC", + "tax_amount": 500, + "description": "Test", + "add_deduct_tax": "Add", + }, + ) + pi.save() + pi.submit() + invoices.append(pi) + + # Third Invoice exceeds single threshold and not exceeding cumulative threshold + pi1 = create_purchase_invoice(supplier="Test TDS Supplier3", rate=6000) + pi1.apply_tds = 1 + pi1.save() + pi1.submit() + invoices.append(pi1) + + # Cumulative threshold is 10,000 + # Threshold calculation should be only on the third invoice + self.assertEqual(pi1.taxes[0].tax_amount, 800) + + for d in reversed(invoices): + d.cancel() + + def test_cumulative_threshold_with_tax_on_excess_amount(self): + invoices = [] + frappe.db.set_value("Supplier", "Test TDS Supplier3", "tax_withholding_category", "New TDS Category") + + # Invoice with tax and without exceeding single and cumulative thresholds + for _ in range(2): + pi = create_purchase_invoice(supplier="Test TDS Supplier3", rate=10000, do_not_save=True) + pi.apply_tds = 1 + pi.append( + "taxes", + { + "category": "Total", + "charge_type": "Actual", + "account_head": "_Test Account VAT - _TC", + "cost_center": "Main - _TC", + "tax_amount": 500, + "description": "Test", + "add_deduct_tax": "Add", + }, + ) + pi.save() + pi.submit() + invoices.append(pi) + + # Third Invoice exceeds single threshold and not exceeding cumulative threshold + pi1 = create_purchase_invoice(supplier="Test TDS Supplier3", rate=20000) + pi1.apply_tds = 1 + pi1.save() + pi1.submit() + invoices.append(pi1) + + # Cumulative threshold is 10,000 + # Threshold calculation should be only on the third invoice + self.assertTrue(len(pi1.taxes) > 0) + self.assertEqual(pi1.taxes[0].tax_amount, 1000) + + for d in reversed(invoices): + d.cancel() + def test_cumulative_threshold_tcs(self): frappe.db.set_value( "Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS" @@ -210,6 +295,46 @@ def test_tcs_on_unallocated_advance_payments(self): d.reload() d.cancel() + def test_tcs_on_allocated_advance_payments(self): + frappe.db.set_value( + "Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS" + ) + + vouchers = [] + + # create advance payment + pe = create_payment_entry( + payment_type="Receive", party_type="Customer", party="Test TCS Customer", paid_amount=30000 + ) + pe.paid_from = "Debtors - _TC" + pe.paid_to = "Cash - _TC" + pe.submit() + vouchers.append(pe) + + si = create_sales_invoice(customer="Test TCS Customer", rate=50000) + advances = si.get_advance_entries() + si.append( + "advances", + { + "reference_type": advances[0].reference_type, + "reference_name": advances[0].reference_name, + "advance_amount": advances[0].amount, + "allocated_amount": 30000, + }, + ) + si.submit() + vouchers.append(si) + + # assert tax collection on total invoice ,advance payment adjusted should be excluded. + tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == "TCS - _TC"]) + # tcs = (inv amt)50000+(adv amt)30000-(adv adj) 30000 - threshold(30000) * rate 10% + self.assertEqual(tcs_charged, 2000) + + # cancel invoice and payments to avoid clashing + for d in reversed(vouchers): + d.reload() + d.cancel() + def test_tds_calculation_on_net_total(self): frappe.db.set_value( "Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS" diff --git a/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py b/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py index 43dfbfaef60f..c058dbfa0b85 100644 --- a/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py +++ b/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py @@ -7,7 +7,9 @@ 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.accounts.test.accounts_mixin import AccountsTestMixin +from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order @@ -260,6 +262,7 @@ def test_04_unreconciliation_on_multi_currency_invoice(self): pe1.paid_from = self.debtors_usd pe1.paid_from_account_currency = "USD" pe1.source_exchange_rate = 75 + pe1.paid_amount = 100 pe1.received_amount = 75 * 100 pe1.save() # Allocate payment against both invoices @@ -277,6 +280,7 @@ def test_04_unreconciliation_on_multi_currency_invoice(self): pe2.paid_from = self.debtors_usd pe2.paid_from_account_currency = "USD" pe2.source_exchange_rate = 75 + pe2.paid_amount = 100 pe2.received_amount = 75 * 100 pe2.save() # Allocate payment against both invoices @@ -360,6 +364,107 @@ def test_05_unreconcile_order(self): # Assert 'Advance Paid' so.reload() pe.reload() - self.assertEqual(so.advance_paid, 0) + self.assertEqual(so.advance_paid, 100) self.assertEqual(len(pe.references), 0) self.assertEqual(pe.unallocated_amount, 100) + + pe.cancel() + so.reload() + self.assertEqual(so.advance_paid, 100) + + def test_06_unreconcile_advance_from_payment_entry(self): + self.enable_advance_as_liability() + so1 = self.create_sales_order() + so2 = self.create_sales_order() + + pe = self.create_payment_entry() + # Allocation payment against Sales Order + pe.paid_amount = 260 + pe.append( + "references", + {"reference_doctype": so1.doctype, "reference_name": so1.name, "allocated_amount": 150}, + ) + pe.append( + "references", + {"reference_doctype": so2.doctype, "reference_name": so2.name, "allocated_amount": 110}, + ) + pe.save().submit() + + # Assert 'Advance Paid' + so1.reload() + self.assertEqual(so1.advance_paid, 150) + so2.reload() + self.assertEqual(so2.advance_paid, 110) + + unreconcile = frappe.get_doc( + { + "doctype": "Unreconcile Payment", + "company": self.company, + "voucher_type": pe.doctype, + "voucher_no": pe.name, + } + ) + unreconcile.add_references() + self.assertEqual(len(unreconcile.allocations), 2) + allocations = [(x.reference_name, x.allocated_amount) for x in unreconcile.allocations] + self.assertListEqual(allocations, [(so1.name, 150), (so2.name, 110)]) + # unreconcile so2 + unreconcile.remove(unreconcile.allocations[0]) + unreconcile.save().submit() + + # Assert 'Advance Paid' + so1.reload() + so2.reload() + pe.reload() + self.assertEqual(so1.advance_paid, 150) + self.assertEqual(so2.advance_paid, 110) + self.assertEqual(len(pe.references), 1) + self.assertEqual(pe.unallocated_amount, 110) + + self.disable_advance_as_liability() + + def test_07_adv_from_so_to_invoice(self): + self.enable_advance_as_liability() + so = self.create_sales_order() + pe = self.create_payment_entry() + pe.paid_amount = 1000 + pe.append( + "references", + {"reference_doctype": so.doctype, "reference_name": so.name, "allocated_amount": 1000}, + ) + pe.save().submit() + + # Assert 'Advance Paid' + so.reload() + self.assertEqual(so.advance_paid, 1000) + + si = make_sales_invoice(so.name) + si.insert().submit() + + pr = frappe.get_doc( + { + "doctype": "Payment Reconciliation", + "company": self.company, + "party_type": "Customer", + "party": so.customer, + } + ) + accounts = get_party_account("Customer", so.customer, so.company, True) + pr.receivable_payable_account = accounts[0] + pr.default_advance_account = accounts[1] + pr.get_unreconciled_entries() + self.assertEqual(len(pr.get("invoices")), 1) + self.assertEqual(len(pr.get("payments")), 1) + invoices = [x.as_dict() for x in pr.get("invoices")] + payments = [x.as_dict() for x in pr.get("payments")] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + + self.assertEqual(len(pr.get("invoices")), 0) + self.assertEqual(len(pr.get("payments")), 0) + + # Assert 'Advance Paid' + so.reload() + self.assertEqual(so.advance_paid, 1000) + + self.disable_advance_as_liability() diff --git a/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.json b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.json index f906dc6cec6a..f2c2f380fe90 100644 --- a/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.json +++ b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.json @@ -1,7 +1,5 @@ { "actions": [], - "allow_rename": 1, - "autoname": "format:UNREC-{#####}", "creation": "2023-08-22 10:26:34.421423", "default_view": "List", "doctype": "DocType", @@ -58,11 +56,10 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-08-28 17:42:50.261377", + "modified": "2024-10-10 12:03:50.022444", "modified_by": "Administrator", "module": "Accounts", "name": "Unreconcile Payment", - "naming_rule": "Expression", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index a4d128a58459..19da840f5436 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -7,7 +7,7 @@ import frappe from frappe import _ from frappe.model.meta import get_field_precision -from frappe.utils import cint, flt, formatdate, getdate, now +from frappe.utils import cint, flt, formatdate, get_link_to_form, getdate, now import erpnext from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( @@ -37,13 +37,14 @@ 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, - cancel=0, - adv_adj=adv_adj, - update_outstanding=update_outstanding, - from_repost=from_repost, - ) + if gl_map[0].voucher_type != "Period Closing Voucher": + create_payment_ledger_entry( + gl_map, + cancel=0, + adv_adj=adv_adj, + update_outstanding=update_outstanding, + from_repost=from_repost, + ) save_entries(gl_map, adv_adj, update_outstanding, from_repost) # Post GL Map proccess there may no be any GL Entries elif gl_map: @@ -116,17 +117,16 @@ def get_accounting_dimensions_for_offsetting_entry(gl_map, company): def validate_disabled_accounts(gl_map): accounts = [d.account for d in gl_map if d.account] - Account = frappe.qb.DocType("Account") - - disabled_accounts = ( - frappe.qb.from_(Account) - .where(Account.name.isin(accounts) & Account.disabled == 1) - .select(Account.name, Account.disabled) - ).run(as_dict=True) + disabled_accounts = frappe.get_all( + "Account", + filters={"disabled": 1, "is_group": 0, "company": gl_map[0].company}, + fields=["name"], + ) - if disabled_accounts: + used_disabled_accounts = set(accounts).intersection(set([d.name for d in disabled_accounts])) + if used_disabled_accounts: account_list = "
" - account_list += ", ".join([frappe.bold(d.name) for d in disabled_accounts]) + account_list += ", ".join([frappe.bold(d) for d in used_disabled_accounts]) frappe.throw( _("Cannot create accounting entries against disabled accounts: {0}").format(account_list), title=_("Disabled Account Selected"), @@ -179,50 +179,53 @@ def process_gl_map(gl_map, merge_entries=True, precision=None): def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None): - cost_center_allocation = get_cost_center_allocation_data(gl_map[0]["company"], gl_map[0]["posting_date"]) - if not cost_center_allocation: - return gl_map - new_gl_map = [] for d in gl_map: cost_center = d.get("cost_center") # Validate budget against main cost center validate_expense_against_budget(d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision)) - - if cost_center and cost_center_allocation.get(cost_center): - for sub_cost_center, percentage in cost_center_allocation.get(cost_center, {}).items(): - gle = copy.deepcopy(d) - gle.cost_center = sub_cost_center - for field in ("debit", "credit", "debit_in_account_currency", "credit_in_account_currency"): - gle[field] = flt(flt(d.get(field)) * percentage / 100, precision) - new_gl_map.append(gle) - else: + cost_center_allocation = get_cost_center_allocation_data( + gl_map[0]["company"], gl_map[0]["posting_date"], cost_center + ) + if not cost_center_allocation: new_gl_map.append(d) + continue + + for sub_cost_center, percentage in cost_center_allocation: + gle = copy.deepcopy(d) + gle.cost_center = sub_cost_center + for field in ("debit", "credit", "debit_in_account_currency", "credit_in_account_currency"): + gle[field] = flt(flt(d.get(field)) * percentage / 100, precision) + new_gl_map.append(gle) return new_gl_map -def get_cost_center_allocation_data(company, posting_date): - par = frappe.qb.DocType("Cost Center Allocation") - child = frappe.qb.DocType("Cost Center Allocation Percentage") +def get_cost_center_allocation_data(company, posting_date, cost_center): + cost_center_allocation = frappe.db.get_value( + "Cost Center Allocation", + { + "docstatus": 1, + "company": company, + "valid_from": ("<=", posting_date), + "main_cost_center": cost_center, + }, + pluck="name", + order_by="valid_from desc", + ) - records = ( - frappe.qb.from_(par) - .inner_join(child) - .on(par.name == child.parent) - .select(par.main_cost_center, child.cost_center, child.percentage) - .where(par.docstatus == 1) - .where(par.company == company) - .where(par.valid_from <= posting_date) - .orderby(par.valid_from, order=frappe.qb.desc) - ).run(as_dict=True) + if not cost_center_allocation: + return [] - cc_allocation = frappe._dict() - for d in records: - cc_allocation.setdefault(d.main_cost_center, frappe._dict()).setdefault(d.cost_center, d.percentage) + records = frappe.db.get_all( + "Cost Center Allocation Percentage", + {"parent": cost_center_allocation}, + ["cost_center", "percentage"], + as_list=True, + ) - return cc_allocation + return records def merge_similar_entries(gl_map, precision=None): @@ -231,6 +234,10 @@ def merge_similar_entries(gl_map, precision=None): merge_properties = get_merge_properties(accounting_dimensions) for entry in gl_map: + if entry._skip_merge: + merged_gl_map.append(entry) + continue + entry.merge_key = get_merge_key(entry, merge_properties) # if there is already an entry in this account then just add it # to that entry @@ -308,66 +315,48 @@ def check_if_in_list(gle, gl_map): def toggle_debit_credit_if_negative(gl_map): + debit_credit_field_map = { + "debit": "credit", + "debit_in_account_currency": "credit_in_account_currency", + "debit_in_transaction_currency": "credit_in_transaction_currency", + } + for entry in gl_map: # toggle debit, credit if negative entry - if flt(entry.debit) < 0 and flt(entry.credit) < 0 and flt(entry.debit) == flt(entry.credit): - entry.credit *= -1 - entry.debit *= -1 - - if ( - flt(entry.debit_in_account_currency) < 0 - and flt(entry.credit_in_account_currency) < 0 - and flt(entry.debit_in_account_currency) == flt(entry.credit_in_account_currency) - ): - entry.credit_in_account_currency *= -1 - entry.debit_in_account_currency *= -1 - - if flt(entry.debit) < 0: - entry.credit = flt(entry.credit) - flt(entry.debit) - entry.debit = 0.0 - - if flt(entry.debit_in_account_currency) < 0: - entry.credit_in_account_currency = flt(entry.credit_in_account_currency) - flt( - entry.debit_in_account_currency - ) - entry.debit_in_account_currency = 0.0 - - if flt(entry.credit) < 0: - entry.debit = flt(entry.debit) - flt(entry.credit) - entry.credit = 0.0 + for debit_field, credit_field in debit_credit_field_map.items(): + debit = flt(entry.get(debit_field)) + credit = flt(entry.get(credit_field)) + + if debit < 0 and credit < 0 and debit == credit: + debit *= -1 + credit *= -1 + + if debit < 0: + credit = credit - debit + debit = 0.0 + + if credit < 0: + debit = debit - credit + credit = 0.0 + + # update net values + # In some scenarios net value needs to be shown in the ledger + # This method updates net values as debit or credit + if entry.post_net_value and debit and credit: + if debit > credit: + debit = debit - credit + credit = 0.0 - if flt(entry.credit_in_account_currency) < 0: - entry.debit_in_account_currency = flt(entry.debit_in_account_currency) - flt( - entry.credit_in_account_currency - ) - entry.credit_in_account_currency = 0.0 + else: + credit = credit - debit + debit = 0.0 - update_net_values(entry) + entry[debit_field] = debit + entry[credit_field] = credit return gl_map -def update_net_values(entry): - # In some scenarios net value needs to be shown in the ledger - # This method updates net values as debit or credit - if entry.post_net_value and entry.debit and entry.credit: - if entry.debit > entry.credit: - entry.debit = entry.debit - entry.credit - entry.debit_in_account_currency = ( - entry.debit_in_account_currency - entry.credit_in_account_currency - ) - entry.credit = 0 - entry.credit_in_account_currency = 0 - else: - entry.credit = entry.credit - entry.debit - entry.credit_in_account_currency = ( - entry.credit_in_account_currency - entry.debit_in_account_currency - ) - - entry.debit = 0 - entry.debit_in_account_currency = 0 - - def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False): if not from_repost: validate_cwip_accounts(gl_map) @@ -489,16 +478,36 @@ def raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_ ) +def has_opening_entries(gl_map: list) -> bool: + for x in gl_map: + if x.is_opening == "Yes": + return True + return False + + def make_round_off_gle(gl_map, debit_credit_diff, precision): - round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( + round_off_account, round_off_cost_center, round_off_for_opening = get_round_off_account_and_cost_center( gl_map[0].company, gl_map[0].voucher_type, gl_map[0].voucher_no ) round_off_gle = frappe._dict() round_off_account_exists = False + has_opening_entry = has_opening_entries(gl_map) + + if has_opening_entry: + if not round_off_for_opening: + frappe.throw( + _("Please set '{0}' in Company: {1}").format( + frappe.bold("Round Off for Opening"), get_link_to_form("Company", gl_map[0].company) + ) + ) + + account = round_off_for_opening + else: + account = round_off_account if gl_map[0].voucher_type != "Period Closing Voucher": for d in gl_map: - if d.account == round_off_account: + if d.account == account: round_off_gle = d if d.debit: debit_credit_diff -= flt(d.debit) - flt(d.credit) @@ -516,7 +525,7 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision): round_off_gle.update( { - "account": round_off_account, + "account": account, "debit_in_account_currency": abs(debit_credit_diff) if debit_credit_diff < 0 else 0, "credit_in_account_currency": debit_credit_diff if debit_credit_diff > 0 else 0, "debit": abs(debit_credit_diff) if debit_credit_diff < 0 else 0, @@ -530,6 +539,9 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision): } ) + if has_opening_entry: + round_off_gle.update({"is_opening": "Yes"}) + update_accounting_dimensions(round_off_gle) if not round_off_account_exists: gl_map.append(round_off_gle) @@ -554,9 +566,9 @@ def update_accounting_dimensions(round_off_gle): def get_round_off_account_and_cost_center(company, voucher_type, voucher_no, use_company_default=False): - round_off_account, round_off_cost_center = frappe.get_cached_value( - "Company", company, ["round_off_account", "round_off_cost_center"] - ) or [None, None] + round_off_account, round_off_cost_center, round_off_for_opening = frappe.get_cached_value( + "Company", company, ["round_off_account", "round_off_cost_center", "round_off_for_opening"] + ) or [None, None, None] # Use expense account as fallback if not round_off_account: @@ -571,12 +583,20 @@ def get_round_off_account_and_cost_center(company, voucher_type, voucher_no, use round_off_cost_center = parent_cost_center if not round_off_account: - frappe.throw(_("Please mention Round Off Account in Company")) + frappe.throw( + _("Please mention '{0}' in Company: {1}").format( + frappe.bold("Round Off Account"), get_link_to_form("Company", company) + ) + ) if not round_off_cost_center: - frappe.throw(_("Please mention Round Off Cost Center in Company")) + frappe.throw( + _("Please mention '{0}' in Company: {1}").format( + frappe.bold("Round Off Cost Center"), get_link_to_form("Company", company) + ) + ) - return round_off_account, round_off_cost_center + return round_off_account, round_off_cost_center, round_off_for_opening def make_reverse_gl_entries( @@ -705,7 +725,7 @@ def validate_against_pcv(is_opening, posting_date, company): ) last_pcv_date = frappe.db.get_value( - "Period Closing Voucher", {"docstatus": 1, "company": company}, "max(posting_date)" + "Period Closing Voucher", {"docstatus": 1, "company": company}, "max(period_end_date)" ) if last_pcv_date and getdate(posting_date) <= getdate(last_pcv_date): diff --git a/erpnext/accounts/number_card/total_incoming_bills/total_incoming_bills.json b/erpnext/accounts/number_card/total_incoming_bills/total_incoming_bills.json index 283e187b5426..88c7cae3f69d 100644 --- a/erpnext/accounts/number_card/total_incoming_bills/total_incoming_bills.json +++ b/erpnext/accounts/number_card/total_incoming_bills/total_incoming_bills.json @@ -4,13 +4,14 @@ "docstatus": 0, "doctype": "Number Card", "document_type": "Purchase Invoice", + "dynamic_filters_json": "[[\"Purchase Invoice\",\"company\",\"=\",\" frappe.defaults.get_user_default(\\\"Company\\\")\"]]", "filters_json": "[[\"Purchase Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Purchase Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]", "function": "Sum", "idx": 0, "is_public": 1, "is_standard": 1, "label": "Total Incoming Bills", - "modified": "2020-07-22 13:06:46.045344", + "modified": "2024-11-20 19:08:37.043777", "modified_by": "Administrator", "module": "Accounts", "name": "Total Incoming Bills", diff --git a/erpnext/accounts/number_card/total_incoming_payment/total_incoming_payment.json b/erpnext/accounts/number_card/total_incoming_payment/total_incoming_payment.json index bc23c15b6a9d..a53b222ed7d4 100644 --- a/erpnext/accounts/number_card/total_incoming_payment/total_incoming_payment.json +++ b/erpnext/accounts/number_card/total_incoming_payment/total_incoming_payment.json @@ -4,6 +4,7 @@ "docstatus": 0, "doctype": "Number Card", "document_type": "Payment Entry", + "dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", "filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\",false],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\",false],[\"Payment Entry\",\"payment_type\",\"=\",\"Receive\",false]]", "function": "Sum", "idx": 0, diff --git a/erpnext/accounts/number_card/total_outgoing_bills/total_outgoing_bills.json b/erpnext/accounts/number_card/total_outgoing_bills/total_outgoing_bills.json index fe9161821027..092defd94bdd 100644 --- a/erpnext/accounts/number_card/total_outgoing_bills/total_outgoing_bills.json +++ b/erpnext/accounts/number_card/total_outgoing_bills/total_outgoing_bills.json @@ -4,6 +4,7 @@ "docstatus": 0, "doctype": "Number Card", "document_type": "Sales Invoice", + "dynamic_filters_json": "[[\"Sales Invoice\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", "filters_json": "[[\"Sales Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Sales Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]", "function": "Sum", "idx": 0, diff --git a/erpnext/accounts/number_card/total_outgoing_payment/total_outgoing_payment.json b/erpnext/accounts/number_card/total_outgoing_payment/total_outgoing_payment.json index d27be8835003..d60f30f7c9a8 100644 --- a/erpnext/accounts/number_card/total_outgoing_payment/total_outgoing_payment.json +++ b/erpnext/accounts/number_card/total_outgoing_payment/total_outgoing_payment.json @@ -4,6 +4,7 @@ "docstatus": 0, "doctype": "Number Card", "document_type": "Payment Entry", + "dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", "filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\",false],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\",false],[\"Payment Entry\",\"payment_type\",\"=\",\"Pay\",false]]", "function": "Sum", "idx": 0, diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index e6015e081d68..5be80872db8b 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -29,6 +29,12 @@ from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen from erpnext.utilities.regional import temporary_flag +try: + from frappe.contacts.doctype.address.address import render_address as _render_address +except ImportError: + # Older frappe versions where this function is not available + from frappe.contacts.doctype.address.address import get_address_display as _render_address + PURCHASE_TRANSACTION_TYPES = { "Supplier Quotation", "Purchase Order", @@ -68,7 +74,7 @@ def get_party_details( pos_profile=None, ): if not party: - return {} + return frappe._dict() if not frappe.db.exists(party_type, party): frappe.throw(_("{0}: {1} does not exists").format(party_type, party)) return _get_party_details( @@ -881,16 +887,17 @@ def get_party_shipping_address(doctype: str, name: str) -> str | None: def get_partywise_advanced_payment_amount( party_type, posting_date=None, future_payment=0, company=None, party=None ): + account_type = frappe.get_cached_value("Party Type", party_type, "account_type") + ple = frappe.qb.DocType("Payment Ledger Entry") + acc = frappe.qb.DocType("Account") + query = ( frappe.qb.from_(ple) - .select(ple.party, Abs(Sum(ple.amount).as_("amount"))) - .where( - (ple.party_type.isin(party_type)) - & (ple.amount < 0) - & (ple.against_voucher_no == ple.voucher_no) - & (ple.delinked == 0) - ) + .inner_join(acc) + .on(ple.account == acc.name) + .select(ple.party) + .where((ple.party_type.isin(party_type)) & (acc.account_type == account_type) & (ple.delinked == 0)) .groupby(ple.party) ) @@ -909,9 +916,32 @@ def get_partywise_advanced_payment_amount( if invoice_doctypes := frappe.get_hooks("invoice_doctypes"): query = query.where(ple.voucher_type.notin(invoice_doctypes)) - data = query.run() - if data: - return frappe._dict(data) + # Get advance amount from Receivable / Payable Account + party_ledger = query.select(Abs(Sum(ple.amount).as_("amount"))) + party_ledger = party_ledger.where(ple.amount < 0) + party_ledger = party_ledger.where(ple.against_voucher_no == ple.voucher_no) + party_ledger = party_ledger.where( + acc.root_type == ("Liability" if account_type == "Payable" else "Asset") + ) + + data = party_ledger.run() + data = frappe._dict(data or {}) + + # Get advance amount from Advance Account + advance_ledger = query.select(Sum(ple.amount).as_("amount"), ple.account) + advance_ledger = advance_ledger.where( + acc.root_type == ("Asset" if account_type == "Payable" else "Liability") + ) + advance_ledger = advance_ledger.groupby(ple.account) + advance_ledger = advance_ledger.having(Sum(ple.amount) < 0) + + advance_data = advance_ledger.run() + + for row in advance_data: + data.setdefault(row[0], 0) + data[row[0]] += abs(row[1]) + + return data def get_default_contact(doctype: str, name: str) -> str | None: @@ -958,10 +988,4 @@ def add_party_account(party_type, party, company, account): def render_address(address, check_permissions=True): - try: - from frappe.contacts.doctype.address.address import render_address as _render - except ImportError: - # Older frappe versions where this function is not available - from frappe.contacts.doctype.address.address import get_address_display as _render - - return frappe.call(_render, address, check_permissions=check_permissions) + return frappe.call(_render_address, address, check_permissions=check_permissions) diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 86463f1f1f26..445e532183b5 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -61,32 +61,10 @@ frappe.query_reports["Accounts Payable"] = { default: "Due Date", }, { - fieldname: "range1", - label: __("Ageing Range 1"), - fieldtype: "Int", - default: "30", - reqd: 1, - }, - { - fieldname: "range2", - label: __("Ageing Range 2"), - fieldtype: "Int", - default: "60", - reqd: 1, - }, - { - fieldname: "range3", - label: __("Ageing Range 3"), - fieldtype: "Int", - default: "90", - reqd: 1, - }, - { - fieldname: "range4", - label: __("Ageing Range 4"), - fieldtype: "Int", - default: "120", - reqd: 1, + fieldname: "range", + label: __("Ageing Range"), + fieldtype: "Data", + default: "30, 60, 90, 120", }, { fieldname: "payment_terms_template", @@ -162,6 +140,11 @@ frappe.query_reports["Accounts Payable"] = { label: __("In Party Currency"), fieldtype: "Check", }, + { + fieldname: "handle_employee_advances", + label: __("Handle Employee Advances"), + fieldtype: "Check", + }, ], formatter: function (value, row, column, data, default_formatter) { diff --git a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py index 43856bf569f0..8971dc3d37bb 100644 --- a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py +++ b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py @@ -30,10 +30,7 @@ def test_accounts_payable_for_foreign_currency_supplier(self): "party_type": "Supplier", "party": [self.supplier], "report_date": today(), - "range1": 30, - "range2": 60, - "range3": 90, - "range4": 120, + "range": "30, 60, 90, 120", "in_party_currency": 1, } diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js index 92ea9e8f598f..cf7a62c6b69d 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js @@ -24,32 +24,10 @@ frappe.query_reports["Accounts Payable Summary"] = { default: "Due Date", }, { - fieldname: "range1", - label: __("Ageing Range 1"), - fieldtype: "Int", - default: "30", - reqd: 1, - }, - { - fieldname: "range2", - label: __("Ageing Range 2"), - fieldtype: "Int", - default: "60", - reqd: 1, - }, - { - fieldname: "range3", - label: __("Ageing Range 3"), - fieldtype: "Int", - default: "90", - reqd: 1, - }, - { - fieldname: "range4", - label: __("Ageing Range 4"), - fieldtype: "Int", - default: "120", - reqd: 1, + fieldname: "range", + label: __("Ageing Range"), + fieldtype: "Data", + default: "30, 60, 90, 120", }, { fieldname: "finance_book", diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index 7e795fbe3c15..9f15bbc333d8 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -89,32 +89,10 @@ frappe.query_reports["Accounts Receivable"] = { default: "Due Date", }, { - fieldname: "range1", - label: __("Ageing Range 1"), - fieldtype: "Int", - default: "30", - reqd: 1, - }, - { - fieldname: "range2", - label: __("Ageing Range 2"), - fieldtype: "Int", - default: "60", - reqd: 1, - }, - { - fieldname: "range3", - label: __("Ageing Range 3"), - fieldtype: "Int", - default: "90", - reqd: 1, - }, - { - fieldname: "range4", - label: __("Ageing Range 4"), - fieldtype: "Int", - default: "120", - reqd: 1, + fieldname: "range", + label: __("Ageing Range"), + fieldtype: "Data", + default: "30, 60, 90, 120", }, { fieldname: "customer_group", diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index f8511d2f4972..49dce0e299b5 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -50,6 +50,11 @@ def __init__(self, filters=None): getdate(nowdate()) if self.filters.report_date > getdate(nowdate()) else self.filters.report_date ) + if not self.filters.range: + self.filters.range = "30, 60, 90, 120" + self.ranges = [num.strip() for num in self.filters.range.split(",") if num.strip().isdigit()] + self.range_numbers = [num for num in range(1, len(self.ranges) + 2)] + def run(self, args): self.filters.update(args) self.set_defaults() @@ -112,6 +117,26 @@ def get_data(self): self.build_data() + def build_voucher_dict(self, ple): + return frappe._dict( + voucher_type=ple.voucher_type, + voucher_no=ple.voucher_no, + party=ple.party, + party_account=ple.account, + posting_date=ple.posting_date, + account_currency=ple.account_currency, + remarks=ple.remarks, + invoiced=0.0, + paid=0.0, + credit_note=0.0, + outstanding=0.0, + invoiced_in_account_currency=0.0, + paid_in_account_currency=0.0, + credit_note_in_account_currency=0.0, + outstanding_in_account_currency=0.0, + cost_center=ple.cost_center, + ) + def init_voucher_balance(self): # build all keys, since we want to exclude vouchers beyond the report date for ple in self.ple_entries: @@ -123,24 +148,8 @@ def init_voucher_balance(self): key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party) if key not in self.voucher_balance: - self.voucher_balance[key] = frappe._dict( - voucher_type=ple.voucher_type, - voucher_no=ple.voucher_no, - party=ple.party, - party_account=ple.account, - posting_date=ple.posting_date, - account_currency=ple.account_currency, - remarks=ple.remarks, - invoiced=0.0, - paid=0.0, - credit_note=0.0, - outstanding=0.0, - invoiced_in_account_currency=0.0, - paid_in_account_currency=0.0, - credit_note_in_account_currency=0.0, - outstanding_in_account_currency=0.0, - cost_center=ple.cost_center, - ) + self.voucher_balance[key] = self.build_voucher_dict(ple) + self.get_invoices(ple) if self.filters.get("group_by_party"): @@ -208,6 +217,18 @@ def get_voucher_balance(self, ple): row = self.voucher_balance.get(key) + # Build and use a separate row for Employee Advances. + # This allows Payments or Journals made against Emp Advance to be processed. + if ( + not row + and ple.against_voucher_type == "Employee Advance" + and self.filters.handle_employee_advances + ): + _d = self.build_voucher_dict(ple) + _d.voucher_type = ple.against_voucher_type + _d.voucher_no = ple.against_voucher_no + row = self.voucher_balance[key] = _d + if not row: # no invoice, this is an invoice / stand-alone payment / credit note if self.filters.get("ignore_accounts"): @@ -289,8 +310,8 @@ def build_data(self): must_consider = False if self.filters.get("for_revaluation_journals"): - if (abs(row.outstanding) >= 0.0 / 10**self.currency_precision) or ( - abs(row.outstanding_in_account_currency) >= 0.0 / 10**self.currency_precision + if (abs(row.outstanding) >= 1.0 / 10**self.currency_precision) or ( + abs(row.outstanding_in_account_currency) >= 1.0 / 10**self.currency_precision ): must_consider = True else: @@ -364,6 +385,7 @@ def build_delivery_note_map(self): self.delivery_notes = frappe._dict() # delivery note link inside sales invoice + # nosemgrep si_against_dn = frappe.db.sql( """ select parent, delivery_note @@ -379,6 +401,7 @@ def build_delivery_note_map(self): if d.delivery_note: self.delivery_notes.setdefault(d.parent, set()).add(d.delivery_note) + # nosemgrep dn_against_si = frappe.db.sql( """ select distinct parent, against_sales_invoice @@ -396,13 +419,16 @@ def build_delivery_note_map(self): def get_invoice_details(self): self.invoice_details = frappe._dict() if self.account_type == "Receivable": + # nosemgrep si_list = frappe.db.sql( """ select name, due_date, po_no from `tabSales Invoice` where posting_date <= %s + and company = %s + and docstatus = 1 """, - self.filters.report_date, + (self.filters.report_date, self.filters.company), as_dict=1, ) for d in si_list: @@ -410,6 +436,7 @@ def get_invoice_details(self): # Get Sales Team if self.filters.show_sales_person: + # nosemgrep sales_team = frappe.db.sql( """ select parent, sales_person @@ -424,25 +451,33 @@ def get_invoice_details(self): ) if self.account_type == "Payable": + # nosemgrep for pi in frappe.db.sql( """ select name, due_date, bill_no, bill_date from `tabPurchase Invoice` - where posting_date <= %s + where + posting_date <= %s + and company = %s + and docstatus = 1 """, - self.filters.report_date, + (self.filters.report_date, self.filters.company), as_dict=1, ): self.invoice_details.setdefault(pi.name, pi) # Invoices booked via Journal Entries + # nosemgrep journal_entries = frappe.db.sql( """ select name, due_date, bill_no, bill_date from `tabJournal Entry` - where posting_date <= %s + where + posting_date <= %s + and company = %s + and docstatus = 1 """, - self.filters.report_date, + (self.filters.report_date, self.filters.company), as_dict=1, ) @@ -451,6 +486,8 @@ def get_invoice_details(self): self.invoice_details.setdefault(je.name, je) def set_party_details(self, row): + if not row.party: + return # customer / supplier name party_details = self.get_party_details(row.party) or {} row.update(party_details) @@ -475,6 +512,7 @@ def allocate_outstanding_based_on_payment_terms(self, row): def get_payment_terms(self, row): # build payment_terms for row + # nosemgrep payment_terms_details = frappe.db.sql( f""" select @@ -484,7 +522,8 @@ def get_payment_terms(self, row): from `tab{row.voucher_type}` si, `tabPayment Schedule` ps where si.name = ps.parent and - si.name = %s + si.name = %s and + si.is_return = 0 order by ps.paid_amount desc, due_date """, row.voucher_no, @@ -687,6 +726,7 @@ def allocate_future_payments(self, row): def get_return_entries(self): doctype = "Sales Invoice" if self.account_type == "Receivable" else "Purchase Invoice" filters = { + "posting_date": ("<=", self.filters.report_date), "is_return": 1, "docstatus": 1, "company": self.filters.company, @@ -717,37 +757,22 @@ def set_ageing(self, row): # ageing buckets should not have amounts if due date is not reached if getdate(entry_date) > getdate(self.filters.report_date): - row.range1 = row.range2 = row.range3 = row.range4 = row.range5 = 0.0 + [setattr(row, f"range{i}", 0.0) for i in self.range_numbers] - row.total_due = row.range1 + row.range2 + row.range3 + row.range4 + row.range5 + row.total_due = sum(row[f"range{i}"] for i in self.range_numbers) def get_ageing_data(self, entry_date, row): # [0-30, 30-60, 60-90, 90-120, 120-above] - row.range1 = row.range2 = row.range3 = row.range4 = row.range5 = 0.0 + [setattr(row, f"range{i}", 0.0) for i in self.range_numbers] if not (self.age_as_on and entry_date): return row.age = (getdate(self.age_as_on) - getdate(entry_date)).days or 0 - index = None - - if not (self.filters.range1 and self.filters.range2 and self.filters.range3 and self.filters.range4): - self.filters.range1, self.filters.range2, self.filters.range3, self.filters.range4 = ( - 30, - 60, - 90, - 120, - ) - for i, days in enumerate( - [self.filters.range1, self.filters.range2, self.filters.range3, self.filters.range4] - ): - if cint(row.age) <= cint(days): - index = i - break - - if index is None: - index = 4 + index = next( + (i for i, days in enumerate(self.ranges) if cint(row.age) <= cint(days)), len(self.ranges) + ) row["range" + str(index + 1)] = row.outstanding def get_ple_entries(self): @@ -809,6 +834,7 @@ def get_sales_invoices_or_customers_based_on_sales_person(self): if self.filters.get("sales_person"): lft, rgt = frappe.db.get_value("Sales Person", self.filters.get("sales_person"), ["lft", "rgt"]) + # nosemgrep records = frappe.db.sql( """ select distinct parent, parenttype @@ -987,22 +1013,29 @@ def get_party_details(self, party): def get_columns(self): self.columns = [] - self.add_column("Posting Date", fieldtype="Date") + self.add_column(_("Posting Date"), fieldname="posting_date", fieldtype="Date") self.add_column( - label="Party Type", + label=_("Party Type"), fieldname="party_type", fieldtype="Data", width=100, ) self.add_column( - label="Party", + label=_("Party"), fieldname="party", fieldtype="Dynamic Link", options="party_type", width=180, ) + if self.account_type == "Receivable": + label = _("Receivable Account") + elif self.account_type == "Payable": + label = _("Payable Account") + else: + label = _("Party Account") + self.add_column( - label=self.account_type + " Account", + label=label, fieldname="party_account", fieldtype="Link", options="Account", @@ -1011,10 +1044,10 @@ def get_columns(self): if self.party_naming_by == "Naming Series": if self.account_type == "Payable": - label = "Supplier Name" + label = _("Supplier Name") fieldname = "supplier_name" else: - label = "Customer Name" + label = _("Customer Name") fieldname = "customer_name" self.add_column( label=label, @@ -1040,7 +1073,7 @@ def get_columns(self): width=180, ) - self.add_column(label="Due Date", fieldtype="Date") + self.add_column(label=_("Due Date"), fieldname="due_date", fieldtype="Date") if self.account_type == "Payable": self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data") @@ -1059,6 +1092,7 @@ def get_columns(self): self.add_column(_("Debit Note"), fieldname="credit_note") self.add_column(_("Outstanding Amount"), fieldname="outstanding") + self.add_column(label=_("Age (Days)"), fieldname="age", fieldtype="Int", width=80) self.setup_ageing_columns() self.add_column( @@ -1117,34 +1151,26 @@ def add_column(self, label, fieldname=None, fieldtype="Currency", options=None, def setup_ageing_columns(self): # for charts self.ageing_column_labels = [] - self.add_column(label=_("Age (Days)"), fieldname="age", fieldtype="Int", width=80) + ranges = [*self.ranges, "Above"] + + prev_range_value = 0 + for idx, curr_range_value in enumerate(ranges): + label = f"{prev_range_value}-{curr_range_value}" + self.add_column(label=label, fieldname="range" + str(idx + 1)) - for i, label in enumerate( - [ - "0-{range1}".format(range1=self.filters["range1"]), - "{range1}-{range2}".format( - range1=cint(self.filters["range1"]) + 1, range2=self.filters["range2"] - ), - "{range2}-{range3}".format( - range2=cint(self.filters["range2"]) + 1, range3=self.filters["range3"] - ), - "{range3}-{range4}".format( - range3=cint(self.filters["range3"]) + 1, range4=self.filters["range4"] - ), - _("{range4}-Above").format(range4=cint(self.filters["range4"]) + 1), - ] - ): - self.add_column(label=label, fieldname="range" + str(i + 1)) self.ageing_column_labels.append(label) + if curr_range_value.isdigit(): + prev_range_value = cint(curr_range_value) + 1 + def get_chart_data(self): + precision = cint(frappe.db.get_default("float_precision")) or 2 rows = [] for row in self.data: row = frappe._dict(row) if not cint(row.bold): - values = [row.range1, row.range2, row.range3, row.range4, row.range5] - precision = cint(frappe.db.get_default("float_precision")) or 2 - rows.append({"values": [flt(val, precision) for val in values]}) + values = [flt(row.get(f"range{i}", None), precision) for i in self.range_numbers] + rows.append({"values": values}) self.chart = { "data": {"labels": self.ageing_column_labels, "datasets": rows}, diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index c4baa4e48423..39ca78153c39 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -83,10 +83,7 @@ def test_pos_receivable(self): "party": [self.customer], "report_date": add_days(today(), 2), "based_on_payment_terms": 0, - "range1": 30, - "range2": 60, - "range3": 90, - "range4": 120, + "range": "30, 60, 90, 120", "show_remarks": False, } @@ -116,10 +113,7 @@ def test_accounts_receivable(self): "company": self.company, "based_on_payment_terms": 1, "report_date": today(), - "range1": 30, - "range2": 60, - "range3": 90, - "range4": 120, + "range": "30, 60, 90, 120", "show_remarks": True, } @@ -172,10 +166,7 @@ def test_cr_note_flag_to_update_self(self): filters = { "company": self.company, "report_date": today(), - "range1": 30, - "range2": 60, - "range3": 90, - "range4": 120, + "range": "30, 60, 90, 120", "show_remarks": True, } @@ -266,10 +257,7 @@ def test_payment_againt_po_in_receivable_report(self): "company": self.company, "based_on_payment_terms": 0, "report_date": today(), - "range1": 30, - "range2": 60, - "range3": 90, - "range4": 120, + "range": "30, 60, 90, 120", } report = execute(filters) @@ -328,10 +316,7 @@ def test_exchange_revaluation_for_party(self): filters = { "company": self.company, "report_date": today(), - "range1": 30, - "range2": 60, - "range3": 90, - "range4": 120, + "range": "30, 60, 90, 120", } report = execute(filters) @@ -397,10 +382,7 @@ def test_payment_against_credit_note(self): filters = { "company": self.company, "report_date": today(), - "range1": 30, - "range2": 60, - "range3": 90, - "range4": 120, + "range": "30, 60, 90, 120", } report = execute(filters) self.assertEqual(report[1], []) @@ -416,10 +398,7 @@ def test_group_by_party(self): filters = { "company": self.company, "report_date": today(), - "range1": 30, - "range2": 60, - "range3": 90, - "range4": 120, + "range": "30, 60, 90, 120", "group_by_party": True, } report = execute(filters)[1] @@ -493,10 +472,7 @@ def test_future_payments(self): filters = { "company": self.company, "report_date": today(), - "range1": 30, - "range2": 60, - "range3": 90, - "range4": 120, + "range": "30, 60, 90, 120", "show_future_payments": True, } report = execute(filters)[1] @@ -555,10 +531,7 @@ def test_sales_person(self): filters = { "company": self.company, "report_date": today(), - "range1": 30, - "range2": 60, - "range3": 90, - "range4": 120, + "range": "30, 60, 90, 120", "sales_person": sales_person.name, "show_sales_person": True, } @@ -575,10 +548,7 @@ def test_cost_center_filter(self): filters = { "company": self.company, "report_date": today(), - "range1": 30, - "range2": 60, - "range3": 90, - "range4": 120, + "range": "30, 60, 90, 120", "cost_center": self.cost_center, } report = execute(filters)[1] @@ -593,10 +563,7 @@ def test_customer_group_filter(self): filters = { "company": self.company, "report_date": today(), - "range1": 30, - "range2": 60, - "range3": 90, - "range4": 120, + "range": "30, 60, 90, 120", "customer_group": cus_group, } report = execute(filters)[1] @@ -618,10 +585,7 @@ def test_multi_customer_group_filter(self): filters = { "company": self.company, "report_date": today(), - "range1": 30, - "range2": 60, - "range3": 90, - "range4": 120, + "range": "30, 60, 90, 120", "customer_group": cus_groups_list, # Use the list of customer groups } report = execute(filters)[1] @@ -660,10 +624,7 @@ def test_party_account_filter(self): filters = { "company": self.company, "report_date": today(), - "range1": 30, - "range2": 60, - "range3": 90, - "range4": 120, + "range": "30, 60, 90, 120", "party_account": self.debit_to, } report = execute(filters)[1] @@ -711,10 +672,7 @@ def test_usd_customer_filter(self): "party_type": "Customer", "party": [self.customer], "report_date": today(), - "range1": 30, - "range2": 60, - "range3": 90, - "range4": 120, + "range": "30, 60, 90, 120", "in_party_currency": 1, } @@ -754,10 +712,7 @@ def test_multi_select_party_filter(self): "party_type": "Customer", "party": [self.customer1, self.customer3], "report_date": today(), - "range1": 30, - "range2": 60, - "range3": 90, - "range4": 120, + "range": "30, 60, 90, 120", } si1 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True) @@ -837,10 +792,7 @@ def test_report_output_if_party_is_missing(self): filters = { "company": self.company, "report_date": today(), - "range1": 30, - "range2": 60, - "range3": 90, - "range4": 120, + "range": "30, 60, 90, 120", } report_ouput = execute(filters)[1] @@ -903,10 +855,7 @@ def test_future_payments_on_foreign_currency(self): { "company": self.company, "report_date": today(), - "range1": 30, - "range2": 60, - "range3": 90, - "range4": 120, + "range": "30, 60, 90, 120", "show_future_payments": True, "in_party_currency": False, } @@ -965,10 +914,7 @@ def test_accounts_receivable_output_for_minor_outstanding(self): filters = { "company": self.company, "report_date": today(), - "range1": 30, - "range2": 60, - "range3": 90, - "range4": 120, + "range": "30, 60, 90, 120", } # check invoice grand total and invoiced column's value for 3 payment terms @@ -991,10 +937,7 @@ def test_cost_center_on_report_output(self): filters = { "company": self.company, "report_date": today(), - "range1": 30, - "range2": 60, - "range3": 90, - "range4": 120, + "range": "30, 60, 90, 120", } # check invoice grand total and invoiced column's value for 3 payment terms diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js index 964abc23747d..e36f40169b34 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js @@ -24,32 +24,10 @@ frappe.query_reports["Accounts Receivable Summary"] = { default: "Due Date", }, { - fieldname: "range1", - label: __("Ageing Range 1"), - fieldtype: "Int", - default: "30", - reqd: 1, - }, - { - fieldname: "range2", - label: __("Ageing Range 2"), - fieldtype: "Int", - default: "60", - reqd: 1, - }, - { - fieldname: "range3", - label: __("Ageing Range 3"), - fieldtype: "Int", - default: "90", - reqd: 1, - }, - { - fieldname: "range4", - label: __("Ageing Range 4"), - fieldtype: "Int", - default: "120", - reqd: 1, + fieldname: "range", + label: __("Ageing Range"), + fieldtype: "Data", + default: "30, 60, 90, 120", }, { fieldname: "finance_book", diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index 6a1b1057724d..87fc7ea68be5 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -104,25 +104,23 @@ def get_party_total(self, args): self.set_party_details(d) def init_party_total(self, row): + default_dict = { + "invoiced": 0.0, + "paid": 0.0, + "credit_note": 0.0, + "outstanding": 0.0, + "total_due": 0.0, + "future_amount": 0.0, + "sales_person": [], + "party_type": row.party_type, + } + for i in self.range_numbers: + range_key = f"range{i}" + default_dict[range_key] = 0.0 + self.party_total.setdefault( row.party, - frappe._dict( - { - "invoiced": 0.0, - "paid": 0.0, - "credit_note": 0.0, - "outstanding": 0.0, - "range1": 0.0, - "range2": 0.0, - "range3": 0.0, - "range4": 0.0, - "range5": 0.0, - "total_due": 0.0, - "future_amount": 0.0, - "sales_person": [], - "party_type": row.party_type, - } - ), + frappe._dict(default_dict), ) def set_party_details(self, row): @@ -173,6 +171,7 @@ def get_columns(self): self.add_column(_("Difference"), fieldname="diff") self.setup_ageing_columns() + self.add_column(label="Total Amount Due", fieldname="total_due") if self.filters.show_future_payments: self.add_column(label=_("Future Payment Amount"), fieldname="future_amount") @@ -206,27 +205,6 @@ def get_columns(self): label=_("Currency"), fieldname="currency", fieldtype="Link", options="Currency", width=80 ) - def setup_ageing_columns(self): - for i, label in enumerate( - [ - "0-{range1}".format(range1=self.filters["range1"]), - "{range1}-{range2}".format( - range1=cint(self.filters["range1"]) + 1, range2=self.filters["range2"] - ), - "{range2}-{range3}".format( - range2=cint(self.filters["range2"]) + 1, range3=self.filters["range3"] - ), - "{range3}-{range4}".format( - range3=cint(self.filters["range3"]) + 1, range4=self.filters["range4"] - ), - "{range4}-{above}".format(range4=cint(self.filters["range4"]) + 1, above=_("Above")), - ] - ): - self.add_column(label=label, fieldname="range" + str(i + 1)) - - # Add column for total due amount - self.add_column(label="Total Amount Due", fieldname="total_due") - def get_gl_balance(report_date, company): return frappe._dict( diff --git a/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py index 4ef607bab286..a98cc6af7a3f 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py @@ -27,10 +27,7 @@ def test_01_receivable_summary_output(self): "company": self.company, "customer": self.customer, "posting_date": today(), - "range1": 30, - "range2": 60, - "range3": 90, - "range4": 120, + "range": "30, 60, 90, 120", } si = create_sales_invoice( @@ -121,10 +118,7 @@ def test_02_various_filters_and_output(self): "company": self.company, "customer": self.customer, "posting_date": today(), - "range1": 30, - "range2": 60, - "range3": 90, - "range4": 120, + "range": "30, 60, 90, 120", } si = create_sales_invoice( diff --git a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py index e1545bdcd879..a21103c719de 100644 --- a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py +++ b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py @@ -89,7 +89,9 @@ def get_data(filters): & (DepreciationSchedule.schedule_date == d.posting_date) ) ).run(as_dict=True) - asset_data.accumulated_depreciation_amount = query[0]["accumulated_depreciation_amount"] + asset_data.accumulated_depreciation_amount = ( + query[0]["accumulated_depreciation_amount"] if query else 0 + ) else: asset_data.accumulated_depreciation_amount += d.debit diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.js b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.js index 49771d7ebe9f..0f74df6f9090 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.js +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.js @@ -46,5 +46,11 @@ frappe.query_reports["Asset Depreciations and Balances"] = { options: "Asset", depends_on: "eval: doc.group_by == 'Asset'", }, + { + fieldname: "finance_book", + label: __("Finance Book"), + fieldtype: "Link", + options: "Finance Book", + }, ], }; diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py index f9a008ade7fd..cdd5baf32409 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py @@ -36,6 +36,7 @@ def get_group_by_asset_category_data(filters): + flt(row.cost_of_new_purchase) - flt(row.cost_of_sold_asset) - flt(row.cost_of_scrapped_asset) + - flt(row.cost_of_capitalized_asset) ) row.update( @@ -68,7 +69,10 @@ def get_group_by_asset_category_data(filters): def get_asset_categories_for_grouped_by_category(filters): condition = "" if filters.get("asset_category"): - condition += " and asset_category = %(asset_category)s" + condition += " and a.asset_category = %(asset_category)s" + if filters.get("finance_book"): + condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)" + # nosemgrep return frappe.db.sql( f""" @@ -108,10 +112,26 @@ def get_asset_categories_for_grouped_by_category(filters): end else 0 - end), 0) as cost_of_scrapped_asset + end), 0) as cost_of_scrapped_asset, + ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 + and a.disposal_date >= %(from_date)s + and a.disposal_date <= %(to_date)s then + case when a.status = "Capitalized" then + a.gross_purchase_amount + else + 0 + end + else + 0 + end), 0) as cost_of_capitalized_asset from `tabAsset` a - where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {condition} - and not exists(select name from `tabAsset Capitalization Asset Item` where asset = a.name) + where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition} + and not exists( + select 1 from `tabAsset Capitalization Asset Item` acai join `tabAsset Capitalization` ac on acai.parent=ac.name + where acai.asset = a.name + and ac.posting_date < %(from_date)s + and ac.docstatus=1 + ) group by a.asset_category """, { @@ -119,6 +139,7 @@ def get_asset_categories_for_grouped_by_category(filters): "from_date": filters.from_date, "company": filters.company, "asset_category": filters.get("asset_category"), + "finance_book": filters.get("finance_book"), }, as_dict=1, ) @@ -127,55 +148,77 @@ def get_asset_categories_for_grouped_by_category(filters): def get_asset_details_for_grouped_by_category(filters): condition = "" if filters.get("asset"): - condition += " and name = %(asset)s" + condition += " and a.name = %(asset)s" + if filters.get("finance_book"): + condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)" + + # nosemgrep return frappe.db.sql( f""" - SELECT name, - ifnull(sum(case when purchase_date < %(from_date)s then - case when ifnull(disposal_date, 0) = 0 or disposal_date >= %(from_date)s then - gross_purchase_amount + SELECT a.name, + ifnull(sum(case when a.purchase_date < %(from_date)s then + case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then + a.gross_purchase_amount else 0 end else 0 end), 0) as cost_as_on_from_date, - ifnull(sum(case when purchase_date >= %(from_date)s then - gross_purchase_amount + ifnull(sum(case when a.purchase_date >= %(from_date)s then + a.gross_purchase_amount else 0 end), 0) as cost_of_new_purchase, - ifnull(sum(case when ifnull(disposal_date, 0) != 0 - and disposal_date >= %(from_date)s - and disposal_date <= %(to_date)s then - case when status = "Sold" then - gross_purchase_amount + ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 + and a.disposal_date >= %(from_date)s + and a.disposal_date <= %(to_date)s then + case when a.status = "Sold" then + a.gross_purchase_amount else 0 end else 0 end), 0) as cost_of_sold_asset, - ifnull(sum(case when ifnull(disposal_date, 0) != 0 - and disposal_date >= %(from_date)s - and disposal_date <= %(to_date)s then - case when status = "Scrapped" then - gross_purchase_amount + ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 + and a.disposal_date >= %(from_date)s + and a.disposal_date <= %(to_date)s then + case when a.status = "Scrapped" then + a.gross_purchase_amount else 0 end else 0 - end), 0) as cost_of_scrapped_asset - from `tabAsset` - where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {condition} - group by name + end), 0) as cost_of_scrapped_asset, + ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 + and a.disposal_date >= %(from_date)s + and a.disposal_date <= %(to_date)s then + case when a.status = "Capitalized" then + a.gross_purchase_amount + else + 0 + end + else + 0 + end), 0) as cost_of_capitalized_asset + from `tabAsset` a + where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition} + and not exists( + select 1 from `tabAsset Capitalization Asset Item` acai join `tabAsset Capitalization` ac on acai.parent=ac.name + where acai.asset = a.name + and ac.posting_date < %(from_date)s + and ac.docstatus=1 + ) + group by a.name """, { "to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company, "asset": filters.get("asset"), + "finance_book": filters.get("finance_book"), }, as_dict=1, ) @@ -197,6 +240,7 @@ def get_group_by_asset_data(filters): + flt(row.cost_of_new_purchase) - flt(row.cost_of_sold_asset) - flt(row.cost_of_scrapped_asset) + - flt(row.cost_of_capitalized_asset) ) row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", ""))) @@ -223,9 +267,15 @@ def get_group_by_asset_data(filters): def get_assets_for_grouped_by_category(filters): condition = "" if filters.get("asset_category"): - condition = " and a.asset_category = '{}'".format(filters.get("asset_category")) + condition = f" and a.asset_category = '{filters.get('asset_category')}'" + finance_book_filter = "" + if filters.get("finance_book"): + finance_book_filter += " and ifnull(gle.finance_book, '')=%(finance_book)s" + condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)" + + # nosemgrep return frappe.db.sql( - """ + f""" SELECT results.asset_category, sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date, sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period, @@ -255,7 +305,14 @@ def get_assets_for_grouped_by_category(filters): aca.parent = a.asset_category and aca.company_name = %(company)s join `tabCompany` company on company.name = %(company)s - where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0} + where + a.docstatus=1 + and a.company=%(company)s + and a.purchase_date <= %(to_date)s + and gle.debit != 0 + and gle.is_cancelled = 0 + and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) + {condition} {finance_book_filter} group by a.asset_category union SELECT a.asset_category, @@ -271,11 +328,16 @@ def get_assets_for_grouped_by_category(filters): end), 0) as depreciation_eliminated_during_the_period, 0 as depreciation_amount_during_the_period from `tabAsset` a - where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0} + where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition} group by a.asset_category) as results group by results.asset_category - """.format(condition), - {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, + """, + { + "to_date": filters.to_date, + "from_date": filters.from_date, + "company": filters.company, + "finance_book": filters.get("finance_book", ""), + }, as_dict=1, ) @@ -283,9 +345,15 @@ def get_assets_for_grouped_by_category(filters): def get_assets_for_grouped_by_asset(filters): condition = "" if filters.get("asset"): - condition = " and a.name = '{}'".format(filters.get("asset")) + condition = f" and a.name = '{filters.get('asset')}'" + finance_book_filter = "" + if filters.get("finance_book"): + finance_book_filter += " and ifnull(gle.finance_book, '')=%(finance_book)s" + condition += " and exists (select 1 from `tabAsset Depreciation Schedule` ads where ads.asset = a.name and ads.finance_book = %(finance_book)s)" + + # nosemgrep return frappe.db.sql( - """ + f""" SELECT results.name as asset, sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date, sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period, @@ -315,7 +383,14 @@ def get_assets_for_grouped_by_asset(filters): aca.parent = a.asset_category and aca.company_name = %(company)s join `tabCompany` company on company.name = %(company)s - where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0} + where + a.docstatus=1 + and a.company=%(company)s + and a.purchase_date <= %(to_date)s + and gle.debit != 0 + and gle.is_cancelled = 0 + and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) + {finance_book_filter} {condition} group by a.name union SELECT a.name as name, @@ -331,11 +406,16 @@ def get_assets_for_grouped_by_asset(filters): end), 0) as depreciation_eliminated_during_the_period, 0 as depreciation_amount_during_the_period from `tabAsset` a - where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0} + where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition} group by a.name) as results group by results.name - """.format(condition), - {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, + """, + { + "to_date": filters.to_date, + "from_date": filters.from_date, + "company": filters.company, + "finance_book": filters.get("finance_book", ""), + }, as_dict=1, ) @@ -389,6 +469,12 @@ def get_columns(filters): "fieldtype": "Currency", "width": 140, }, + { + "label": _("Cost of New Capitalized Asset"), + "fieldname": "cost_of_capitalized_asset", + "fieldtype": "Currency", + "width": 140, + }, { "label": _("Cost as on") + " " + formatdate(filters.to_date), "fieldname": "cost_as_on_to_date", diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.py b/erpnext/accounts/report/balance_sheet/balance_sheet.py index e89a177a8671..fc19c40f8f98 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.py +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.py @@ -7,6 +7,7 @@ from frappe.utils import cint, flt from erpnext.accounts.report.financial_statements import ( + compute_growth_view_data, get_columns, get_data, get_filtered_list_for_consolidated_report, @@ -95,12 +96,15 @@ def execute(filters=None): filters.periodicity, period_list, filters.accumulated_values, company=filters.company ) - chart = get_chart_data(filters, columns, asset, liability, equity) + chart = get_chart_data(filters, columns, asset, liability, equity, currency) report_summary, primitive_summary = get_report_summary( period_list, asset, liability, equity, provisional_profit_loss, currency, filters ) + if filters.get("selected_view") == "Growth": + compute_growth_view_data(data, period_list) + return columns, data, message, chart, report_summary, primitive_summary @@ -122,13 +126,13 @@ def get_provisional_profit_loss( for period in period_list: key = period if consolidated else period.key - total_assets = flt(asset[0].get(key)) + total_assets = flt(asset[-2].get(key)) effective_liability = 0.00 - if liability: - effective_liability += flt(liability[0].get(key)) - if equity: - effective_liability += flt(equity[0].get(key)) + if liability and liability[-1] == {}: + effective_liability += flt(liability[-2].get(key)) + if equity and equity[-1] == {}: + effective_liability += flt(equity[-2].get(key)) provisional_profit_loss[key] = total_assets - effective_liability total_row[key] = provisional_profit_loss[key] + effective_liability @@ -195,9 +199,9 @@ def get_report_summary( key = period if consolidated else period.key if asset: net_asset += asset[-2].get(key) - if liability: + if liability and liability[-1] == {}: net_liability += liability[-2].get(key) - if equity: + if equity and equity[-1] == {}: net_equity += equity[-2].get(key) if provisional_profit_loss: net_provisional_profit_loss += provisional_profit_loss.get(key) @@ -221,7 +225,7 @@ def get_report_summary( ], (net_asset - net_liability + net_equity) -def get_chart_data(filters, columns, asset, liability, equity): +def get_chart_data(filters, columns, asset, liability, equity, currency): labels = [d.get("label") for d in columns[2:]] asset_data, liability_data, equity_data = [], [], [] @@ -249,4 +253,8 @@ def get_chart_data(filters, columns, asset, liability, equity): else: chart["type"] = "line" + chart["fieldtype"] = "Currency" + chart["options"] = "currency" + chart["currency"] = currency + return chart diff --git a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.js b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.js index efcfa7a5ee59..2684c87a22ab 100644 --- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.js +++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.js @@ -46,4 +46,20 @@ frappe.query_reports["Bank Reconciliation Statement"] = { fieldtype: "Check", }, ], + formatter: function (value, row, column, data, default_formatter, filter) { + if (column.fieldname == "payment_entry" && value == __("Cheques and Deposits incorrectly cleared")) { + column.link_onclick = + "frappe.query_reports['Bank Reconciliation Statement'].open_utility_report()"; + } + return default_formatter(value, row, column, data); + }, + open_utility_report: function () { + frappe.route_options = { + company: frappe.query_report.get_filter_value("company"), + account: frappe.query_report.get_filter_value("account"), + report_date: frappe.query_report.get_filter_value("report_date"), + }; + frappe.open_in_new_tab = true; + frappe.set_route("query-report", "Cheques and Deposits Incorrectly cleared"); + }, }; 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 8a8e3a599723..c7dba16492e7 100644 --- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py +++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py @@ -154,8 +154,8 @@ def get_payment_entries(filters): select "Payment Entry" as payment_document, name as payment_entry, reference_no, reference_date as ref_date, - if(paid_to=%(account)s, received_amount, 0) as debit, - if(paid_from=%(account)s, paid_amount, 0) as credit, + if(paid_to=%(account)s, received_amount_after_tax, 0) as debit, + if(paid_from=%(account)s, paid_amount_after_tax, 0) as credit, posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date, if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency from `tabPayment Entry` diff --git a/erpnext/accounts/report/cash_flow/cash_flow.js b/erpnext/accounts/report/cash_flow/cash_flow.js index c824f0d9f384..bc76ee0a114d 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.js +++ b/erpnext/accounts/report/cash_flow/cash_flow.js @@ -1,7 +1,10 @@ // Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.query_reports["Cash Flow"] = $.extend({}, erpnext.financial_statements); +frappe.query_reports["Cash Flow"] = $.extend(erpnext.financial_statements, { + name_field: "section", + parent_field: "parent_section", +}); erpnext.utils.add_dimensions("Cash Flow", 10); diff --git a/erpnext/accounts/report/cash_flow/cash_flow.py b/erpnext/accounts/report/cash_flow/cash_flow.py index c034f95ec002..562ac5efb818 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.py +++ b/erpnext/accounts/report/cash_flow/cash_flow.py @@ -30,7 +30,7 @@ def execute(filters=None): company=filters.company, ) - cash_flow_accounts = get_cash_flow_accounts() + cash_flow_sections = get_cash_flow_accounts() # compute net profit / loss income = get_data( @@ -60,14 +60,14 @@ def execute(filters=None): summary_data = {} company_currency = frappe.get_cached_value("Company", filters.company, "default_currency") - for cash_flow_account in cash_flow_accounts: + for cash_flow_section in cash_flow_sections: section_data = [] data.append( { - "account_name": cash_flow_account["section_header"], - "parent_account": None, + "section_name": "'" + cash_flow_section["section_header"] + "'", + "parent_section": None, "indent": 0.0, - "account": cash_flow_account["section_header"], + "section": cash_flow_section["section_header"], } ) @@ -75,31 +75,40 @@ def execute(filters=None): # add first net income in operations section if net_profit_loss: net_profit_loss.update( - {"indent": 1, "parent_account": cash_flow_accounts[0]["section_header"]} + {"indent": 1, "parent_section": cash_flow_sections[0]["section_header"]} ) data.append(net_profit_loss) section_data.append(net_profit_loss) - for account in cash_flow_account["account_types"]: - account_data = get_account_type_based_data( - filters.company, account["account_type"], period_list, filters.accumulated_values, filters + for row in cash_flow_section["account_types"]: + row_data = get_account_type_based_data( + filters.company, row["account_type"], period_list, filters.accumulated_values, filters ) - account_data.update( + accounts = frappe.get_all( + "Account", + filters={ + "account_type": row["account_type"], + "is_group": 0, + }, + pluck="name", + ) + row_data.update( { - "account_name": account["label"], - "account": account["label"], + "section_name": row["label"], + "section": row["label"], "indent": 1, - "parent_account": cash_flow_account["section_header"], + "accounts": accounts, + "parent_section": cash_flow_section["section_header"], "currency": company_currency, } ) - data.append(account_data) - section_data.append(account_data) + data.append(row_data) + section_data.append(row_data) add_total_row_account( data, section_data, - cash_flow_account["section_footer"], + cash_flow_section["section_footer"], period_list, company_currency, summary_data, @@ -109,9 +118,9 @@ def execute(filters=None): add_total_row_account( data, data, _("Net Change in Cash"), period_list, company_currency, summary_data, filters ) - columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company) + columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company, True) - chart = get_chart_data(columns, data) + chart = get_chart_data(columns, data, company_currency) report_summary = get_report_summary(summary_data, company_currency) @@ -217,8 +226,8 @@ def get_start_date(period, accumulated_values, company): def add_total_row_account(out, data, label, period_list, currency, summary_data, filters, consolidated=False): total_row = { - "account_name": "'" + _("{0}").format(label) + "'", - "account": "'" + _("{0}").format(label) + "'", + "section_name": "'" + _("{0}").format(label) + "'", + "section": "'" + _("{0}").format(label) + "'", "currency": currency, } @@ -229,7 +238,7 @@ def add_total_row_account(out, data, label, period_list, currency, summary_data, period_list = get_filtered_list_for_consolidated_report(filters, period_list) for row in data: - if row.get("parent_account"): + if row.get("parent_section"): for period in period_list: key = period if consolidated else period["key"] total_row.setdefault(key, 0.0) @@ -252,20 +261,23 @@ def get_report_summary(summary_data, currency): return report_summary -def get_chart_data(columns, data): +def get_chart_data(columns, data, currency): labels = [d.get("label") for d in columns[2:]] + print(data) datasets = [ { - "name": account.get("account").replace("'", ""), - "values": [account.get(d.get("fieldname")) for d in columns[2:]], + "name": section.get("section").replace("'", ""), + "values": [section.get(d.get("fieldname")) for d in columns[2:]], } - for account in data - if account.get("parent_account") is None and account.get("currency") + for section in data + if section.get("parent_section") is None and section.get("currency") ] datasets = datasets[:-1] chart = {"data": {"labels": labels, "datasets": datasets}, "type": "bar"} chart["fieldtype"] = "Currency" + chart["options"] = "currency" + chart["currency"] = currency return chart diff --git a/erpnext/accounts/report/cheques_and_deposits_incorrectly_cleared/__init__.py b/erpnext/accounts/report/cheques_and_deposits_incorrectly_cleared/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/accounts/report/cheques_and_deposits_incorrectly_cleared/cheques_and_deposits_incorrectly_cleared.js b/erpnext/accounts/report/cheques_and_deposits_incorrectly_cleared/cheques_and_deposits_incorrectly_cleared.js new file mode 100644 index 000000000000..e83fc6f5b57b --- /dev/null +++ b/erpnext/accounts/report/cheques_and_deposits_incorrectly_cleared/cheques_and_deposits_incorrectly_cleared.js @@ -0,0 +1,44 @@ +// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.query_reports["Cheques and Deposits Incorrectly cleared"] = { + filters: [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + reqd: 1, + default: frappe.defaults.get_user_default("Company"), + }, + { + fieldname: "account", + label: __("Bank Account"), + fieldtype: "Link", + options: "Account", + default: frappe.defaults.get_user_default("Company") + ? locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"] + : "", + reqd: 1, + get_query: function () { + var company = frappe.query_report.get_filter_value("company"); + return { + query: "erpnext.controllers.queries.get_account_list", + filters: [ + ["Account", "account_type", "in", "Bank, Cash"], + ["Account", "is_group", "=", 0], + ["Account", "disabled", "=", 0], + ["Account", "company", "=", company], + ], + }; + }, + }, + { + fieldname: "report_date", + label: __("Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, + }, + ], +}; diff --git a/erpnext/accounts/report/cheques_and_deposits_incorrectly_cleared/cheques_and_deposits_incorrectly_cleared.json b/erpnext/accounts/report/cheques_and_deposits_incorrectly_cleared/cheques_and_deposits_incorrectly_cleared.json new file mode 100644 index 000000000000..50cf765ca3d4 --- /dev/null +++ b/erpnext/accounts/report/cheques_and_deposits_incorrectly_cleared/cheques_and_deposits_incorrectly_cleared.json @@ -0,0 +1,29 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2024-07-30 17:20:07.570971", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letterhead": null, + "modified": "2024-07-30 17:20:07.570971", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Cheques and Deposits Incorrectly cleared", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Payment Entry", + "report_name": "Cheques and Deposits Incorrectly cleared", + "report_type": "Script Report", + "roles": [ + { + "role": "Accounts User" + }, + { + "role": "Accounts Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/accounts/report/cheques_and_deposits_incorrectly_cleared/cheques_and_deposits_incorrectly_cleared.py b/erpnext/accounts/report/cheques_and_deposits_incorrectly_cleared/cheques_and_deposits_incorrectly_cleared.py new file mode 100644 index 000000000000..891dc2c4bb10 --- /dev/null +++ b/erpnext/accounts/report/cheques_and_deposits_incorrectly_cleared/cheques_and_deposits_incorrectly_cleared.py @@ -0,0 +1,153 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _, qb +from frappe.query_builder import CustomFunction +from frappe.query_builder.custom import ConstantColumn + + +def execute(filters=None): + columns = get_columns() + data = build_data(filters) + return columns, data + + +def build_payment_entry_dict(row: dict) -> dict: + row_dict = frappe._dict() + row_dict.update( + { + "payment_document": row.get("doctype"), + "payment_entry": row.get("name"), + "posting_date": row.get("posting_date"), + "clearance_date": row.get("clearance_date"), + } + ) + if row.get("payment_type") == "Receive" and row.get("party_type") in ["Customer", "Supplier"]: + row_dict.update( + { + "debit": row.get("amount"), + "credit": 0, + } + ) + else: + row_dict.update( + { + "debit": 0, + "credit": row.get("amount"), + } + ) + return row_dict + + +def build_journal_entry_dict(row: dict) -> dict: + row_dict = frappe._dict() + row_dict.update( + { + "payment_document": row.get("doctype"), + "payment_entry": row.get("name"), + "posting_date": row.get("posting_date"), + "clearance_date": row.get("clearance_date"), + "debit": row.get("debit_in_account_currency"), + "credit": row.get("credit_in_account_currency"), + } + ) + return row_dict + + +def build_data(filters): + vouchers = get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filters) + data = [] + for x in vouchers: + if x.doctype == "Payment Entry": + data.append(build_payment_entry_dict(x)) + elif x.doctype == "Journal Entry": + data.append(build_journal_entry_dict(x)) + return data + + +def get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filters): + je = qb.DocType("Journal Entry") + jea = qb.DocType("Journal Entry Account") + doctype_name = ConstantColumn("Journal Entry") + + journals = ( + qb.from_(je) + .inner_join(jea) + .on(je.name == jea.parent) + .select( + doctype_name.as_("doctype"), + je.name, + jea.debit_in_account_currency, + jea.credit_in_account_currency, + je.posting_date, + je.clearance_date, + ) + .where( + je.docstatus.eq(1) + & jea.account.eq(filters.account) + & je.posting_date.gt(filters.report_date) + & je.clearance_date.lte(filters.report_date) + & (je.is_opening.isnull() | je.is_opening.eq("No")) + ) + .run(as_dict=1) + ) + + ifelse = CustomFunction("IF", ["condition", "then", "else"]) + pe = qb.DocType("Payment Entry") + doctype_name = ConstantColumn("Payment Entry") + payments = ( + qb.from_(pe) + .select( + doctype_name.as_("doctype"), + pe.name, + ifelse(pe.paid_from.eq(filters.account), pe.paid_amount, pe.received_amount).as_("amount"), + pe.payment_type, + pe.party_type, + pe.posting_date, + pe.clearance_date, + ) + .where( + pe.docstatus.eq(1) + & (pe.paid_from.eq(filters.account) | pe.paid_to.eq(filters.account)) + & pe.posting_date.gt(filters.report_date) + & pe.clearance_date.lte(filters.report_date) + ) + .run(as_dict=1) + ) + + return journals + payments + + +def get_columns(): + return [ + { + "fieldname": "payment_document", + "label": _("Payment Document Type"), + "fieldtype": "Data", + "width": 220, + }, + { + "fieldname": "payment_entry", + "label": _("Payment Document"), + "fieldtype": "Dynamic Link", + "options": "payment_document", + "width": 220, + }, + { + "fieldname": "debit", + "label": _("Debit"), + "fieldtype": "Currency", + "options": "account_currency", + "width": 120, + }, + { + "fieldname": "credit", + "label": _("Credit"), + "fieldtype": "Currency", + "options": "account_currency", + "width": 120, + }, + {"fieldname": "posting_date", "label": _("Posting Date"), "fieldtype": "Date", "width": 110}, + {"fieldname": "clearance_date", "label": _("Clearance Date"), "fieldtype": "Date", "width": 110}, + ] diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 2931b728a424..d287b30bf0be 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -115,7 +115,7 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters): True, ) - chart = get_chart_data(filters, columns, asset, liability, equity) + chart = get_chart_data(filters, columns, asset, liability, equity, company_currency) return data, message, chart, report_summary @@ -173,7 +173,7 @@ def get_profit_loss_data(fiscal_year, companies, columns, filters): if net_profit_loss: data.append(net_profit_loss) - chart = get_pl_chart_data(filters, columns, income, expense, net_profit_loss) + chart = get_pl_chart_data(filters, columns, income, expense, net_profit_loss, company_currency) report_summary, primitive_summary = get_pl_summary( companies, "", income, expense, net_profit_loss, company_currency, filters, True @@ -469,10 +469,13 @@ def update_parent_account_names(accounts): for d in accounts: if d.account_number: - account_name = d.account_number + " - " + d.account_name + account_key = d.account_number + " - " + d.account_name else: - account_name = d.account_name - name_to_account_map[d.name] = account_name + account_key = d.account_name + + d.account_key = account_key + + name_to_account_map[d.name] = account_key for account in accounts: if account.parent_account: @@ -505,33 +508,26 @@ def get_subsidiary_companies(company): def get_accounts(root_type, companies): accounts = [] - added_accounts = [] for company in companies: - for account in frappe.get_all( - "Account", - fields=[ - "name", - "is_group", - "company", - "parent_account", - "lft", - "rgt", - "root_type", - "report_type", - "account_name", - "account_number", - ], - filters={"company": company, "root_type": root_type}, - ): - if account.account_number: - account_key = account.account_number + "-" + account.account_name - else: - account_key = account.account_name - - if account_key not in added_accounts: - accounts.append(account) - added_accounts.append(account_key) + accounts.extend( + frappe.get_all( + "Account", + fields=[ + "name", + "is_group", + "company", + "parent_account", + "lft", + "rgt", + "root_type", + "report_type", + "account_name", + "account_number", + ], + filters={"company": company, "root_type": root_type}, + ) + ) return accounts @@ -770,15 +766,17 @@ def add_total_row(out, root_type, balance_must_be, companies, company_currency): def filter_accounts(accounts, depth=10): parent_children_map = {} accounts_by_name = {} + added_accounts = [] + for d in accounts: - if d.account_number: - account_name = d.account_number + " - " + d.account_name - else: - account_name = d.account_name + if d.account_key in added_accounts: + continue + + added_accounts.append(d.account_key) d["company_wise_opening_bal"] = defaultdict(float) - accounts_by_name[account_name] = d + accounts_by_name[d.account_key] = d - parent_children_map.setdefault(d.parent_account or None, []).append(d) + parent_children_map.setdefault(d.parent_account_name or None, []).append(d) filtered_accounts = [] @@ -790,7 +788,7 @@ def add_to_list(parent, level): for child in children: child.indent = level filtered_accounts.append(child) - add_to_list(child.name, level + 1) + add_to_list(child.account_key, level + 1) add_to_list(None, 0) diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py index c6d9eac59662..377777ab2a35 100644 --- a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py +++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py @@ -122,21 +122,24 @@ def simulate_future_posting(self): """ simulate future posting by creating dummy gl entries. starts from the last posting date. """ - if self.service_start_date != self.service_end_date: - if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date: - self.estimate_for_period_list = get_period_list( - self.filters.from_fiscal_year, - self.filters.to_fiscal_year, - add_days(self.last_entry_date, 1), - self.period_list[-1].to_date, - "Date Range", - "Monthly", - company=self.filters.company, - ) - for period in self.estimate_for_period_list: - amount = self.calculate_amount(period.from_date, period.to_date) - gle = self.make_dummy_gle(period.key, period.to_date, amount) - self.gle_entries.append(gle) + if ( + self.service_start_date != self.service_end_date + and add_days(self.last_entry_date, 1) < self.service_end_date + ): + self.estimate_for_period_list = get_period_list( + self.filters.from_fiscal_year, + self.filters.to_fiscal_year, + add_days(self.last_entry_date, 1), + self.service_end_date, + "Date Range", + "Monthly", + company=self.filters.company, + ) + + for period in self.estimate_for_period_list: + amount = self.calculate_amount(period.from_date, period.to_date) + gle = self.make_dummy_gle(period.key, period.to_date, amount) + self.gle_entries.append(gle) def calculate_item_revenue_expense_for_period(self): """ diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 6d7635979bb7..73e49983fb20 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt +import copy import functools import math import re @@ -9,6 +10,7 @@ import frappe from frappe import _ from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate +from pypika.terms import ExistsCriterion from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, @@ -181,12 +183,12 @@ def get_data( company, period_list[0]["year_start_date"] if only_current_fiscal_year else None, period_list[-1]["to_date"], - root.lft, - root.rgt, filters, gl_entries_by_account, - ignore_closing_entries=ignore_closing_entries, + root.lft, + root.rgt, root_type=root_type, + ignore_closing_entries=ignore_closing_entries, ) calculate_values( @@ -333,8 +335,8 @@ def filter_out_zero_value_rows(data, parent_children_map, show_zero_values=False def add_total_row(out, root_type, balance_must_be, period_list, company_currency): total_row = { - "account_name": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)), - "account": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)), + "account_name": "'" + _("Total {0} ({1})").format(_(root_type), _(balance_must_be)) + "'", + "account": "'" + _("Total {0} ({1})").format(_(root_type), _(balance_must_be)) + "'", "currency": company_currency, "opening_balance": 0.0, } @@ -419,93 +421,78 @@ def set_gl_entries_by_account( company, from_date, to_date, - root_lft, - root_rgt, filters, gl_entries_by_account, + root_lft=None, + root_rgt=None, + root_type=None, ignore_closing_entries=False, ignore_opening_entries=False, - root_type=None, ): """Returns a dict like { "account": [gl entries], ... }""" gl_entries = [] - account_filters = { - "company": company, - "is_group": 0, - "lft": (">=", root_lft), - "rgt": ("<=", root_rgt), - } - - if root_type: - account_filters.update( - { - "root_type": root_type, - } - ) - - accounts_list = frappe.db.get_all( - "Account", - filters=account_filters, - pluck="name", + # For balance sheet + ignore_closing_balances = frappe.db.get_single_value( + "Accounts Settings", "ignore_account_closing_balance" ) - - if accounts_list: - # For balance sheet - ignore_closing_balances = frappe.db.get_single_value( - "Accounts Settings", "ignore_account_closing_balance" + if not from_date and not ignore_closing_balances: + last_period_closing_voucher = frappe.db.get_all( + "Period Closing Voucher", + filters={ + "docstatus": 1, + "company": filters.company, + "period_end_date": ("<", filters["period_start_date"]), + }, + fields=["period_end_date", "name"], + order_by="period_end_date desc", + limit=1, ) - if not from_date and not ignore_closing_balances: - last_period_closing_voucher = frappe.db.get_all( - "Period Closing Voucher", - filters={ - "docstatus": 1, - "company": filters.company, - "posting_date": ("<", filters["period_start_date"]), - }, - fields=["posting_date", "name"], - order_by="posting_date desc", - limit=1, + if last_period_closing_voucher: + gl_entries += get_accounting_entries( + "Account Closing Balance", + from_date, + to_date, + filters, + root_lft, + root_rgt, + root_type, + ignore_closing_entries, + last_period_closing_voucher[0].name, ) - if last_period_closing_voucher: - gl_entries += get_accounting_entries( - "Account Closing Balance", - from_date, - to_date, - accounts_list, - filters, - ignore_closing_entries, - last_period_closing_voucher[0].name, - ) - from_date = add_days(last_period_closing_voucher[0].posting_date, 1) - ignore_opening_entries = True - - gl_entries += get_accounting_entries( - "GL Entry", - from_date, - to_date, - accounts_list, - filters, - ignore_closing_entries, - ignore_opening_entries=ignore_opening_entries, - ) + from_date = add_days(last_period_closing_voucher[0].period_end_date, 1) + ignore_opening_entries = True + + gl_entries += get_accounting_entries( + "GL Entry", + from_date, + to_date, + filters, + root_lft, + root_rgt, + root_type, + ignore_closing_entries, + ignore_opening_entries=ignore_opening_entries, + ) - if filters and filters.get("presentation_currency"): - convert_to_presentation_currency(gl_entries, get_currency(filters)) + if filters and filters.get("presentation_currency"): + convert_to_presentation_currency(gl_entries, get_currency(filters)) - for entry in gl_entries: - gl_entries_by_account.setdefault(entry.account, []).append(entry) + for entry in gl_entries: + gl_entries_by_account.setdefault(entry.account, []).append(entry) - return gl_entries_by_account + return gl_entries_by_account def get_accounting_entries( doctype, from_date, to_date, - accounts, filters, - ignore_closing_entries, + root_lft=None, + root_rgt=None, + root_type=None, + ignore_closing_entries=None, period_closing_voucher=None, ignore_opening_entries=False, ): @@ -535,13 +522,30 @@ def get_accounting_entries( query = query.where(gl_entry.period_closing_voucher == period_closing_voucher) query = apply_additional_conditions(doctype, query, from_date, ignore_closing_entries, filters) - query = query.where(gl_entry.account.isin(accounts)) + + if (root_lft and root_rgt) or root_type: + account_filter_query = get_account_filter_query(root_lft, root_rgt, root_type, gl_entry) + query = query.where(ExistsCriterion(account_filter_query)) entries = query.run(as_dict=True) return entries +def get_account_filter_query(root_lft, root_rgt, root_type, gl_entry): + acc = frappe.qb.DocType("Account") + exists_query = ( + frappe.qb.from_(acc).select(acc.name).where(acc.name == gl_entry.account).where(acc.is_group == 0) + ) + if root_lft and root_rgt: + exists_query = exists_query.where(acc.lft >= root_lft).where(acc.rgt <= root_rgt) + + if root_type: + exists_query = exists_query.where(acc.root_type == root_type) + + return exists_query + + def apply_additional_conditions(doctype, query, from_date, ignore_closing_entries, filters): gl_entry = frappe.qb.DocType(doctype) accounting_dimensions = get_accounting_dimensions(as_list=False) @@ -613,11 +617,11 @@ def get_cost_centers_with_children(cost_centers): return list(set(all_cost_centers)) -def get_columns(periodicity, period_list, accumulated_values=1, company=None): +def get_columns(periodicity, period_list, accumulated_values=1, company=None, cash_flow=False): columns = [ { "fieldname": "account", - "label": _("Account"), + "label": _("Account") if not cash_flow else _("Section"), "fieldtype": "Link", "options": "Account", "width": 300, @@ -665,3 +669,67 @@ def get_filtered_list_for_consolidated_report(filters, period_list): filtered_summary_list.append(period) return filtered_summary_list + + +def compute_growth_view_data(data, columns): + data_copy = copy.deepcopy(data) + + for row_idx in range(len(data_copy)): + for column_idx in range(1, len(columns)): + previous_period_key = columns[column_idx - 1].get("key") + current_period_key = columns[column_idx].get("key") + current_period_value = data_copy[row_idx].get(current_period_key) + previous_period_value = data_copy[row_idx].get(previous_period_key) + annual_growth = 0 + + if current_period_value is None: + data[row_idx][current_period_key] = None + continue + + if previous_period_value == 0 and current_period_value > 0: + annual_growth = 1 + + elif previous_period_value > 0: + annual_growth = (current_period_value - previous_period_value) / previous_period_value + + growth_percent = round(annual_growth * 100, 2) + + data[row_idx][current_period_key] = growth_percent + + +def compute_margin_view_data(data, columns, accumulated_values): + if not columns: + return + + if not accumulated_values: + columns.append({"key": "total"}) + + data_copy = copy.deepcopy(data) + + base_row = None + for row in data_copy: + if row.get("account_name") == _("Income"): + base_row = row + break + + if not base_row: + return + + for row_idx in range(len(data_copy)): + # Taking the total income from each column (for all the financial years) as the base (100%) + row = data_copy[row_idx] + if not row: + continue + + for column in columns: + curr_period = column.get("key") + base_value = base_row[curr_period] + curr_value = row[curr_period] + + if curr_value is None or base_value <= 0: + data[row_idx][curr_period] = None + continue + + margin_percent = round((curr_value / base_value) * 100, 2) + + data[row_idx][curr_period] = margin_percent diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py index 89cf7e504f0c..9d079eb9ebd2 100644 --- a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py +++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py @@ -199,8 +199,7 @@ def get_columns(self): dict( label=_("Voucher Type"), fieldname="voucher_type", - fieldtype="Link", - options="DocType", + fieldtype="Data", width="100", ) ) @@ -219,8 +218,7 @@ def get_columns(self): dict( label=_("Party Type"), fieldname="party_type", - fieldtype="Link", - options="DocType", + fieldtype="Data", width="100", ) ) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.html b/erpnext/accounts/report/general_ledger/general_ledger.html index 3c4e1a05c97a..bdea568bdf4e 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.html +++ b/erpnext/accounts/report/general_ledger/general_ledger.html @@ -48,8 +48,9 @@

{% } %} -
{%= __("Remarks") %}: {%= data[i].remarks %} - {% if(data[i].bill_no) { %} + {% if(data[i].remarks) { %} +
{%= __("Remarks") %}: {%= data[i].remarks %} + {% } else if(data[i].bill_no) { %}
{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %} {% } %} diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 2564eb0800f2..69e3d241f126 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -345,10 +345,18 @@ def get_accounts_with_children(accounts): return frappe.qb.from_(doctype).select(doctype.name).where(Criterion.any(conditions)).run(pluck=True) +def set_bill_no(gl_entries): + inv_details = get_supplier_invoice_details() + for gl in gl_entries: + gl["bill_no"] = inv_details.get(gl.get("against_voucher"), "") + + def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries): data = [] totals_dict = get_totals_dict() + set_bill_no(gl_entries) + gle_map = initialize_gle_map(gl_entries, filters, totals_dict) totals, entries = get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, totals_dict) @@ -362,16 +370,21 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension if acc_dict.entries: # opening data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None}) - if filters.get("group_by") != "Group by Voucher": + if (not filters.get("group_by") and not filters.get("voucher_no")) or ( + filters.get("group_by") and filters.get("group_by") != "Group by Voucher" + ): data.append(acc_dict.totals.opening) data += acc_dict.entries # totals - data.append(acc_dict.totals.total) + if filters.get("group_by") or not filters.voucher_no: + data.append(acc_dict.totals.total) # closing - if filters.get("group_by") != "Group by Voucher": + if (not filters.get("group_by") and not filters.get("voucher_no")) or ( + filters.get("group_by") and filters.get("group_by") != "Group by Voucher" + ): data.append(acc_dict.totals.closing) data.append({"debit_in_transaction_currency": None, "credit_in_transaction_currency": None}) @@ -536,7 +549,6 @@ def get_account_type_map(company): def get_result_as_list(data, filters): balance, _balance_in_account_currency = 0, 0 - inv_details = get_supplier_invoice_details() for d in data: if not d.get("posting_date"): @@ -546,7 +558,6 @@ def get_result_as_list(data, filters): d["balance"] = balance d["account_currency"] = filters.account_currency - d["bill_no"] = inv_details.get(d.get("against_voucher"), "") return data diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index a9039a9cadac..c59a3bd2a7ae 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -421,10 +421,10 @@ def __init__(self, filters=None): self.load_invoice_items() self.get_delivery_notes() + self.load_product_bundle() if filters.group_by == "Invoice": self.group_items_by_invoice() - self.load_product_bundle() self.load_non_stock_items() self.get_returned_invoice_items() self.process() @@ -440,6 +440,7 @@ def process(self): if grouped_by_invoice: buying_amount = 0 + base_amount = 0 for row in reversed(self.si_list): if self.filters.get("group_by") == "Monthly": @@ -480,12 +481,11 @@ def process(self): else: row.buying_amount = flt(self.get_buying_amount(row, row.item_code), self.currency_precision) - if grouped_by_invoice: - if row.indent == 1.0: - buying_amount += row.buying_amount - elif row.indent == 0.0: - row.buying_amount = buying_amount - buying_amount = 0 + if grouped_by_invoice and row.indent == 0.0: + row.buying_amount = buying_amount + row.base_amount = base_amount + buying_amount = 0 + base_amount = 0 # get buying rate if flt(row.qty): @@ -495,11 +495,19 @@ def process(self): if self.is_not_invoice_row(row): row.buying_rate, row.base_rate = 0.0, 0.0 + if self.is_not_invoice_row(row): + self.update_return_invoices(row) + + if grouped_by_invoice and row.indent == 1.0: + buying_amount += row.buying_amount + base_amount += row.base_amount + # calculate gross profit row.gross_profit = flt(row.base_amount - row.buying_amount, self.currency_precision) if row.base_amount: row.gross_profit_percent = flt( - (row.gross_profit / row.base_amount) * 100.0, self.currency_precision + (row.gross_profit / row.base_amount) * 100.0, + self.currency_precision, ) else: row.gross_profit_percent = 0.0 @@ -510,33 +518,29 @@ def process(self): if self.grouped: self.get_average_rate_based_on_group_by() + def update_return_invoices(self, row): + if row.parent in self.returned_invoices and row.item_code in self.returned_invoices[row.parent]: + returned_item_rows = self.returned_invoices[row.parent][row.item_code] + for returned_item_row in returned_item_rows: + # returned_items 'qty' should be stateful + if returned_item_row.qty != 0: + if row.qty >= abs(returned_item_row.qty): + row.qty += returned_item_row.qty + row.base_amount += flt(returned_item_row.base_amount, self.currency_precision) + returned_item_row.qty = 0 + returned_item_row.base_amount = 0 + + else: + row.qty = 0 + row.base_amount = 0 + returned_item_row.qty += row.qty + returned_item_row.base_amount += row.base_amount + + row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision) + def get_average_rate_based_on_group_by(self): for key in list(self.grouped): - if self.filters.get("group_by") == "Invoice": - for row in self.grouped[key]: - if row.indent == 1.0: - if ( - row.parent in self.returned_invoices - and row.item_code in self.returned_invoices[row.parent] - ): - returned_item_rows = self.returned_invoices[row.parent][row.item_code] - for returned_item_row in returned_item_rows: - # returned_items 'qty' should be stateful - if returned_item_row.qty != 0: - if row.qty >= abs(returned_item_row.qty): - row.qty += returned_item_row.qty - returned_item_row.qty = 0 - else: - row.qty = 0 - returned_item_row.qty += row.qty - row.base_amount += flt(returned_item_row.base_amount, self.currency_precision) - row.buying_amount = flt( - flt(row.qty) * flt(row.buying_rate), self.currency_precision - ) - 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": + if self.filters.get("group_by") == "Payment Term": for i, row in enumerate(self.grouped[key]): invoice_portion = 0 @@ -556,7 +560,7 @@ def get_average_rate_based_on_group_by(self): new_row = self.set_average_rate(new_row) self.grouped_data.append(new_row) - else: + elif self.filters.get("group_by") != "Invoice": for i, row in enumerate(self.grouped[key]): if i == 0: new_row = row @@ -632,6 +636,7 @@ def get_buying_amount_from_product_bundle(self, row, product_bundle): if packed_item.get("parent_detail_docname") == row.item_row: packed_item_row = row.copy() packed_item_row.warehouse = packed_item.warehouse + packed_item_row.qty = packed_item.total_qty * -1 buying_amount += self.get_buying_amount(packed_item_row, packed_item.item_code) return flt(buying_amount, self.currency_precision) @@ -664,7 +669,9 @@ def get_buying_amount(self, row, item_code): else: my_sle = self.get_stock_ledger_entries(item_code, row.warehouse) if (row.update_stock or row.dn_detail) and my_sle: - parenttype, parent = row.parenttype, row.parent + parenttype = row.parenttype + parent = row.invoice or row.parent + if row.dn_detail: parenttype, parent = "Delivery Note", row.delivery_note @@ -847,6 +854,7 @@ def load_invoice_items(self): `tabSales Invoice`.project, `tabSales Invoice`.update_stock, `tabSales Invoice`.customer, `tabSales Invoice`.customer_group, `tabSales Invoice`.territory, `tabSales Invoice Item`.item_code, + `tabSales Invoice`.base_net_total as "invoice_base_net_total", `tabSales Invoice Item`.item_name, `tabSales Invoice Item`.description, `tabSales Invoice Item`.warehouse, `tabSales Invoice Item`.item_group, `tabSales Invoice Item`.brand, `tabSales Invoice Item`.so_detail, @@ -907,6 +915,7 @@ def group_items_by_invoice(self): """ grouped = OrderedDict() + product_bundles = self.product_bundles.get("Sales Invoice", {}) for row in self.si_list: # initialize list with a header row for each new parent @@ -917,8 +926,7 @@ def group_items_by_invoice(self): ) # if item is a bundle, add it's components as seperate rows - if frappe.db.exists("Product Bundle", row.item_code): - bundled_items = self.get_bundle_items(row) + if bundled_items := product_bundles.get(row.parent, {}).get(row.item_code): for x in bundled_items: bundle_item = self.get_bundle_item_row(row, x) grouped.get(row.parent).append(bundle_item) @@ -954,47 +962,40 @@ def get_invoice_row(self, row): "item_row": None, "is_return": row.is_return, "cost_center": row.cost_center, - "base_net_amount": frappe.db.get_value("Sales Invoice", row.parent, "base_net_total"), + "base_net_amount": row.invoice_base_net_total, } ) - def get_bundle_items(self, product_bundle): - return frappe.get_all( - "Product Bundle Item", filters={"parent": product_bundle.item_code}, fields=["item_code", "qty"] - ) - - def get_bundle_item_row(self, product_bundle, item): - item_name, description, item_group, brand = self.get_bundle_item_details(item.item_code) - + def get_bundle_item_row(self, row, item): return frappe._dict( { - "parent_invoice": product_bundle.item_code, - "indent": product_bundle.indent + 1, + "parent_invoice": row.item_code, + "parenttype": row.parenttype, + "indent": row.indent + 1, "parent": None, "invoice_or_item": item.item_code, - "posting_date": product_bundle.posting_date, - "posting_time": product_bundle.posting_time, - "project": product_bundle.project, - "customer": product_bundle.customer, - "customer_group": product_bundle.customer_group, + "posting_date": row.posting_date, + "posting_time": row.posting_time, + "project": row.project, + "customer": row.customer, + "customer_group": row.customer_group, "item_code": item.item_code, - "item_name": item_name, - "description": description, - "warehouse": product_bundle.warehouse, - "item_group": item_group, - "brand": brand, - "dn_detail": product_bundle.dn_detail, - "delivery_note": product_bundle.delivery_note, - "qty": (flt(product_bundle.qty) * flt(item.qty)), - "item_row": None, - "is_return": product_bundle.is_return, - "cost_center": product_bundle.cost_center, + "item_name": item.item_name, + "description": item.description, + "warehouse": item.warehouse or row.warehouse, + "update_stock": row.update_stock, + "item_group": "", + "brand": "", + "dn_detail": row.dn_detail, + "delivery_note": row.delivery_note, + "qty": item.total_qty * -1, + "item_row": row.item_row, + "is_return": row.is_return, + "cost_center": row.cost_center, + "invoice": row.parent, } ) - def get_bundle_item_details(self, item_code): - return frappe.db.get_value("Item", item_code, ["item_name", "description", "item_group", "brand"]) - def get_stock_ledger_entries(self, item_code, warehouse): if item_code and warehouse: if (item_code, warehouse) not in self.sle: diff --git a/erpnext/accounts/report/gross_profit/test_gross_profit.py b/erpnext/accounts/report/gross_profit/test_gross_profit.py index 83de93891fef..721be79ed88c 100644 --- a/erpnext/accounts/report/gross_profit/test_gross_profit.py +++ b/erpnext/accounts/report/gross_profit/test_gross_profit.py @@ -418,12 +418,12 @@ def test_crnote_against_invoice_with_multiple_instances_of_same_item(self): "item_name": self.item, "warehouse": "Stores - _GP", "qty": 0.0, - "avg._selling_rate": 0.0, + "avg._selling_rate": 100, "valuation_rate": 0.0, - "selling_amount": -100.0, + "selling_amount": 0.0, "buying_amount": 0.0, - "gross_profit": -100.0, - "gross_profit_%": 100.0, + "gross_profit": 0.0, + "gross_profit_%": 0.0, } gp_entry = [x for x in data if x.parent_invoice == sinv.name] # Both items of Invoice should have '0' qty diff --git a/erpnext/accounts/report/invalid_ledger_entries/__init__.py b/erpnext/accounts/report/invalid_ledger_entries/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js new file mode 100644 index 000000000000..47d478f28655 --- /dev/null +++ b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.js @@ -0,0 +1,51 @@ +// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +function get_filters() { + let filters = [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, + }, + { + fieldname: "from_date", + label: __("Start Date"), + fieldtype: "Date", + reqd: 1, + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + }, + { + fieldname: "to_date", + label: __("End Date"), + fieldtype: "Date", + reqd: 1, + default: frappe.datetime.get_today(), + }, + { + fieldname: "account", + label: __("Account"), + fieldtype: "MultiSelectList", + options: "Account", + get_data: function (txt) { + return frappe.db.get_link_options("Account", txt, { + company: frappe.query_report.get_filter_value("company"), + }); + }, + }, + { + fieldname: "voucher_no", + label: __("Voucher No"), + fieldtype: "Data", + width: 100, + }, + ]; + return filters; +} + +frappe.query_reports["Invalid Ledger Entries"] = { + filters: get_filters(), +}; diff --git a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.json b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.json new file mode 100644 index 000000000000..00dbbfc50563 --- /dev/null +++ b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.json @@ -0,0 +1,23 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2024-09-09 12:31:25.295976", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letterhead": null, + "modified": "2024-09-09 12:31:25.295976", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Invalid Ledger Entries", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "GL Entry", + "report_name": "Invalid Ledger Entries", + "report_type": "Script Report", + "roles": [], + "timeout": 0 +} \ No newline at end of file diff --git a/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py new file mode 100644 index 000000000000..33fda705cf24 --- /dev/null +++ b/erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py @@ -0,0 +1,137 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _, qb +from frappe.query_builder import Criterion +from frappe.query_builder.custom import ConstantColumn + + +def execute(filters: dict | None = None): + """Return columns and data for the report. + + This is the main entry point for the report. It accepts the filters as a + dictionary and should return columns and data. It is called by the framework + every time the report is refreshed or a filter is updated. + """ + validate_filters(filters) + + columns = get_columns() + data = get_data(filters) + + return columns, data + + +def get_columns() -> list[dict]: + """Return columns for the report. + + One field definition per column, just like a DocType field definition. + """ + return [ + {"label": _("Voucher Type"), "fieldname": "voucher_type", "fieldtype": "Link", "options": "DocType"}, + { + "label": _("Voucher No"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", + }, + ] + + +def get_data(filters) -> list[list]: + """Return data for the report. + + The report data is a list of rows, with each row being a list of cell values. + """ + active_vouchers = get_active_vouchers_for_period(filters) + invalid_vouchers = identify_cancelled_vouchers(active_vouchers) + + return invalid_vouchers + + +def identify_cancelled_vouchers(active_vouchers: list[dict] | list | None = None) -> list[dict]: + cancelled_vouchers = [] + if active_vouchers: + # Group by voucher types and use single query to identify cancelled vouchers + vtypes = set([x.voucher_type for x in active_vouchers]) + + for _t in vtypes: + _names = [x.voucher_no for x in active_vouchers if x.voucher_type == _t] + dt = qb.DocType(_t) + non_active_vouchers = ( + qb.from_(dt) + .select(ConstantColumn(_t).as_("voucher_type"), dt.name.as_("voucher_no")) + .where(dt.docstatus.ne(1) & dt.name.isin(_names)) + .run(as_dict=True) + ) + if non_active_vouchers: + cancelled_vouchers.extend(non_active_vouchers) + return cancelled_vouchers + + +def validate_filters(filters: dict | None = None): + if not filters: + frappe.throw(_("Filters missing")) + + if not filters.company: + frappe.throw(_("Company is mandatory")) + + if filters.from_date > filters.to_date: + frappe.throw(_("Start Date should be lower than End Date")) + + +def build_query_filters(filters: dict | None = None) -> list: + qb_filters = [] + if filters: + if filters.account: + qb_filters.append(qb.Field("account").isin(filters.account)) + + if filters.voucher_no: + qb_filters.append(qb.Field("voucher_no").eq(filters.voucher_no)) + + return qb_filters + + +def get_active_vouchers_for_period(filters: dict | None = None) -> list[dict]: + uniq_vouchers = [] + + if filters: + gle = qb.DocType("GL Entry") + ple = qb.DocType("Payment Ledger Entry") + + qb_filters = build_query_filters(filters) + + gl_vouchers = ( + qb.from_(gle) + .select(gle.voucher_type) + .distinct() + .select(gle.voucher_no) + .distinct() + .where( + gle.is_cancelled.eq(0) + & gle.company.eq(filters.company) + & gle.posting_date[filters.from_date : filters.to_date] + ) + .where(Criterion.all(qb_filters)) + .run(as_dict=True) + ) + + pl_vouchers = ( + qb.from_(ple) + .select(ple.voucher_type) + .distinct() + .select(ple.voucher_no) + .distinct() + .where( + ple.delinked.eq(0) + & ple.company.eq(filters.company) + & ple.posting_date[filters.from_date : filters.to_date] + ) + .where(Criterion.all(qb_filters)) + .run(as_dict=True) + ) + + uniq_vouchers.extend(gl_vouchers) + uniq_vouchers.extend(pl_vouchers) + + return uniq_vouchers diff --git a/erpnext/accounts/report/payment_ledger/payment_ledger.py b/erpnext/accounts/report/payment_ledger/payment_ledger.py index 9852c6e7ab99..9dd5ae5c400b 100644 --- a/erpnext/accounts/report/payment_ledger/payment_ledger.py +++ b/erpnext/accounts/report/payment_ledger/payment_ledger.py @@ -130,6 +130,7 @@ def get_data(self): ) def get_columns(self): + company_currency = frappe.get_cached_value("Company", self.filters.get("company"), "default_currency") options = None self.columns.append( dict( @@ -194,7 +195,7 @@ def get_columns(self): label=_("Amount"), fieldname="amount", fieldtype="Currency", - options="Company:company:default_currency", + options=company_currency, width="100", ) ) @@ -210,7 +211,7 @@ def get_columns(self): ) ) self.columns.append( - dict(label=_("Currency"), fieldname="currency", fieldtype="Currency", hidden=True) + dict(label=_("Currency"), fieldname="currency", fieldtype="Link", options="Currency", hidden=True) ) def run(self): diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py index 58610b22a93c..2b6280c74b51 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py @@ -7,6 +7,8 @@ from frappe.utils import flt from erpnext.accounts.report.financial_statements import ( + compute_growth_view_data, + compute_margin_view_data, get_columns, get_data, get_filtered_list_for_consolidated_report, @@ -59,15 +61,21 @@ def execute(filters=None): columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company) - chart = get_chart_data(filters, columns, income, expense, net_profit_loss) - currency = filters.presentation_currency or frappe.get_cached_value( "Company", filters.company, "default_currency" ) + chart = get_chart_data(filters, columns, income, expense, net_profit_loss, currency) + report_summary, primitive_summary = get_report_summary( period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters ) + if filters.get("selected_view") == "Growth": + compute_growth_view_data(data, period_list) + + if filters.get("selected_view") == "Margin": + compute_margin_view_data(data, period_list, filters.accumulated_values) + return columns, data, None, chart, report_summary, primitive_summary @@ -152,7 +160,7 @@ def get_net_profit_loss(income, expense, period_list, company, currency=None, co return net_profit_loss -def get_chart_data(filters, columns, income, expense, net_profit_loss): +def get_chart_data(filters, columns, income, expense, net_profit_loss, currency): labels = [d.get("label") for d in columns[2:]] income_data, expense_data, net_profit = [], [], [] @@ -181,5 +189,7 @@ def get_chart_data(filters, columns, income, expense, net_profit_loss): chart["type"] = "line" chart["fieldtype"] = "Currency" + chart["options"] = "currency" + chart["currency"] = currency return chart diff --git a/erpnext/accounts/report/purchase_register/purchase_register.py b/erpnext/accounts/report/purchase_register/purchase_register.py index 504c74babcb3..48364cc2c911 100644 --- a/erpnext/accounts/report/purchase_register/purchase_register.py +++ b/erpnext/accounts/report/purchase_register/purchase_register.py @@ -311,6 +311,7 @@ def get_account_columns(invoice_list, include_payments): """select distinct expense_account from `tabPurchase Invoice Item` where docstatus = 1 and (expense_account is not null and expense_account != '') + and parenttype='Purchase Invoice' and parent in (%s) order by expense_account""" % ", ".join(["%s"] * len(invoice_list)), tuple([inv.name for inv in invoice_list]), @@ -451,7 +452,7 @@ def get_invoice_expense_map(invoice_list): """ select parent, expense_account, sum(base_net_amount) as amount from `tabPurchase Invoice Item` - where parent in (%s) + where parent in (%s) and parenttype='Purchase Invoice' group by parent, expense_account """ % ", ".join(["%s"] * len(invoice_list)), @@ -522,7 +523,7 @@ def get_invoice_po_pr_map(invoice_list): """ select parent, purchase_order, purchase_receipt, po_detail, project from `tabPurchase Invoice Item` - where parent in (%s) + where parent in (%s) and parenttype='Purchase Invoice' """ % ", ".join(["%s"] * len(invoice_list)), tuple(inv.name for inv in invoice_list), diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py index 6c0bf91e3f8b..34d53238f50b 100644 --- a/erpnext/accounts/report/sales_register/sales_register.py +++ b/erpnext/accounts/report/sales_register/sales_register.py @@ -526,7 +526,8 @@ def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts, inclu tax_details = frappe.db.sql( """select parent, account_head, sum(base_tax_amount_after_discount_amount) as tax_amount - from `tabSales Taxes and Charges` where parent in (%s) group by parent, account_head""" + from `tabSales Taxes and Charges` where parent in (%s) and parenttype = 'Sales Invoice' + group by parent, account_head""" % ", ".join(["%s"] * len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1, diff --git a/erpnext/accounts/report/sales_register/test_sales_register.py b/erpnext/accounts/report/sales_register/test_sales_register.py new file mode 100644 index 000000000000..95aa5add24cf --- /dev/null +++ b/erpnext/accounts/report/sales_register/test_sales_register.py @@ -0,0 +1,179 @@ +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import getdate, today + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.report.sales_register.sales_register import execute +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin + + +class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase): + def setUp(self): + self.create_company() + self.create_customer() + self.create_item() + self.create_child_cost_center() + + def tearDown(self): + frappe.db.rollback() + + def create_child_cost_center(self): + cc_name = "South Wing" + if frappe.db.exists("Cost Center", cc_name): + cc = frappe.get_doc("Cost Center", cc_name) + else: + parent = frappe.db.get_value("Cost Center", self.cost_center, "parent_cost_center") + cc = frappe.get_doc( + { + "doctype": "Cost Center", + "company": self.company, + "is_group": False, + "parent_cost_center": parent, + "cost_center_name": cc_name, + } + ) + cc = cc.save() + self.south_cc = cc.name + + def create_sales_invoice(self, rate=100, do_not_submit=False): + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=rate, + price_list_rate=rate, + do_not_save=1, + ) + si = si.save() + if not do_not_submit: + si = si.submit() + return si + + def test_basic_report_output(self): + si = self.create_sales_invoice(rate=98) + + filters = frappe._dict({"from_date": today(), "to_date": today(), "company": self.company}) + report = execute(filters) + + res = [x for x in report[1] if x.get("voucher_no") == si.name] + + expected_result = { + "voucher_type": si.doctype, + "voucher_no": si.name, + "posting_date": getdate(), + "customer": self.customer, + "receivable_account": self.debit_to, + "net_total": 98.0, + "grand_total": 98.0, + "debit": 98.0, + } + + report_output = {k: v for k, v in res[0].items() if k in expected_result} + self.assertDictEqual(report_output, expected_result) + + def test_journal_with_cost_center_filter(self): + je1 = frappe.get_doc( + { + "doctype": "Journal Entry", + "voucher_type": "Journal Entry", + "company": self.company, + "posting_date": getdate(), + "accounts": [ + { + "account": self.debit_to, + "party_type": "Customer", + "party": self.customer, + "credit_in_account_currency": 77, + "credit": 77, + "is_advance": "Yes", + "cost_center": self.cost_center, + }, + { + "account": self.cash, + "debit_in_account_currency": 77, + "debit": 77, + }, + ], + } + ) + je1.submit() + + je2 = frappe.get_doc( + { + "doctype": "Journal Entry", + "voucher_type": "Journal Entry", + "company": self.company, + "posting_date": getdate(), + "accounts": [ + { + "account": self.debit_to, + "party_type": "Customer", + "party": self.customer, + "credit_in_account_currency": 98, + "credit": 98, + "is_advance": "Yes", + "cost_center": self.south_cc, + }, + { + "account": self.cash, + "debit_in_account_currency": 98, + "debit": 98, + }, + ], + } + ) + je2.submit() + + filters = frappe._dict( + { + "from_date": today(), + "to_date": today(), + "company": self.company, + "include_payments": True, + "customer": self.customer, + "cost_center": self.cost_center, + } + ) + report_output = execute(filters)[1] + filtered_output = [x for x in report_output if x.get("voucher_no") == je1.name] + self.assertEqual(len(filtered_output), 1) + expected_result = { + "voucher_type": je1.doctype, + "voucher_no": je1.name, + "posting_date": je1.posting_date, + "customer": self.customer, + "receivable_account": self.debit_to, + "net_total": 77.0, + "credit": 77.0, + } + result_fields = {k: v for k, v in filtered_output[0].items() if k in expected_result} + self.assertDictEqual(result_fields, expected_result) + + filters = frappe._dict( + { + "from_date": today(), + "to_date": today(), + "company": self.company, + "include_payments": True, + "customer": self.customer, + "cost_center": self.south_cc, + } + ) + report_output = execute(filters)[1] + filtered_output = [x for x in report_output if x.get("voucher_no") == je2.name] + self.assertEqual(len(filtered_output), 1) + expected_result = { + "voucher_type": je2.doctype, + "voucher_no": je2.name, + "posting_date": je2.posting_date, + "customer": self.customer, + "receivable_account": self.debit_to, + "net_total": 98.0, + "credit": 98.0, + } + result_output = {k: v for k, v in filtered_output[0].items() if k in expected_result} + self.assertDictEqual(result_output, expected_result) diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py index 276e95bd5d1d..0c18dac77682 100644 --- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py @@ -336,7 +336,7 @@ def get_tds_docs(filters): def get_tds_docs_query(filters, bank_accounts, tds_accounts): if not tds_accounts: frappe.throw( - _("No {0} Accounts found for this company.").format(frappe.bold("Tax Withholding")), + _("No {0} Accounts found for this company.").format(frappe.bold(_("Tax Withholding"))), title=_("Accounts Missing Error"), ) gle = frappe.qb.DocType("GL Entry") diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index f216ecea15a1..8ca850f301e4 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -94,12 +94,6 @@ def get_data(filters): accounts, accounts_by_name, parent_children_map = filter_accounts(accounts) - min_lft, max_rgt = frappe.db.sql( - """select min(lft), max(rgt) from `tabAccount` - where company=%s""", - (filters.company,), - )[0] - gl_entries_by_account = {} opening_balances = get_opening_balances(filters) @@ -112,10 +106,10 @@ def get_data(filters): filters.company, filters.from_date, filters.to_date, - min_lft, - max_rgt, filters, gl_entries_by_account, + root_lft=None, + root_rgt=None, ignore_closing_entries=not flt(filters.with_period_closing_entry_for_current_period), ignore_opening_entries=True, ) @@ -150,9 +144,9 @@ def get_rootwise_opening_balances(filters, report_type): if not ignore_closing_balances: last_period_closing_voucher = frappe.db.get_all( "Period Closing Voucher", - filters={"docstatus": 1, "company": filters.company, "posting_date": ("<", filters.from_date)}, - fields=["posting_date", "name"], - order_by="posting_date desc", + filters={"docstatus": 1, "company": filters.company, "period_end_date": ("<", filters.from_date)}, + fields=["period_end_date", "name"], + order_by="period_end_date desc", limit=1, ) @@ -168,8 +162,8 @@ def get_rootwise_opening_balances(filters, report_type): ) # Report getting generate from the mid of a fiscal year - if getdate(last_period_closing_voucher[0].posting_date) < getdate(add_days(filters.from_date, -1)): - start_date = add_days(last_period_closing_voucher[0].posting_date, 1) + if getdate(last_period_closing_voucher[0].period_end_date) < getdate(add_days(filters.from_date, -1)): + start_date = add_days(last_period_closing_voucher[0].period_end_date, 1) gle += get_opening_balance( "GL Entry", filters, report_type, accounting_dimensions, start_date=start_date ) diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py index bd1b35559eaa..2a72b10e4eb0 100644 --- a/erpnext/accounts/report/utils.py +++ b/erpnext/accounts/report/utils.py @@ -255,7 +255,9 @@ def get_journal_entries(filters, args): ) .orderby(je.posting_date, je.name, order=Order.desc) ) - query = apply_common_conditions(filters, query, doctype="Journal Entry", payments=True) + query = apply_common_conditions( + filters, query, doctype="Journal Entry", child_doctype="Journal Entry Account", payments=True + ) journal_entries = query.run(as_dict=True) return journal_entries @@ -306,7 +308,9 @@ def apply_common_conditions(filters, query, doctype, child_doctype=None, payment query = query.where(parent_doc.posting_date <= filters.to_date) if payments: - if filters.get("cost_center"): + if doctype == "Journal Entry" and filters.get("cost_center"): + query = query.where(child_doc.cost_center == filters.cost_center) + elif filters.get("cost_center"): query = query.where(parent_doc.cost_center == filters.cost_center) else: if filters.get("cost_center"): @@ -326,6 +330,7 @@ def apply_common_conditions(filters, query, doctype, child_doctype=None, payment if join_required: query = query.inner_join(child_doc).on(parent_doc.name == child_doc.parent) + query = query.where(child_doc.parenttype == doctype) query = query.distinct() if parent_doc.get_table_name() != "tabJournal Entry": diff --git a/erpnext/accounts/test/accounts_mixin.py b/erpnext/accounts/test/accounts_mixin.py index d503f7bc4af3..e526e07c7345 100644 --- a/erpnext/accounts/test/accounts_mixin.py +++ b/erpnext/accounts/test/accounts_mixin.py @@ -87,6 +87,22 @@ def create_company(self, company_name="_Test Company", abbr="_TC"): "parent_account": "Bank Accounts - " + abbr, } ), + frappe._dict( + { + "attribute_name": "advance_received", + "account_name": "Advance Received", + "parent_account": "Current Liabilities - " + abbr, + "account_type": "Receivable", + } + ), + frappe._dict( + { + "attribute_name": "advance_paid", + "account_name": "Advance Paid", + "parent_account": "Current Assets - " + abbr, + "account_type": "Payable", + } + ), ] for acc in other_accounts: acc_name = acc.account_name + " - " + abbr @@ -101,9 +117,31 @@ def create_company(self, company_name="_Test Company", abbr="_TC"): "company": self.company, } ) + new_acc.account_type = acc.get("account_type", None) new_acc.save() setattr(self, acc.attribute_name, new_acc.name) + self.identify_default_warehouses() + + def enable_advance_as_liability(self): + company = frappe.get_doc("Company", self.company) + company.book_advance_payments_in_separate_party_account = True + company.default_advance_received_account = self.advance_received + company.default_advance_paid_account = self.advance_paid + company.save() + + def disable_advance_as_liability(self): + company = frappe.get_doc("Company", self.company) + company.book_advance_payments_in_separate_party_account = False + company.default_advance_paid_account = company.default_advance_received_account = None + company.save() + + def identify_default_warehouses(self): + for w in frappe.db.get_all( + "Warehouse", filters={"company": self.company}, fields=["name", "warehouse_name"] + ): + setattr(self, "warehouse_" + w.warehouse_name.lower().strip().replace(" ", "_"), w.name) + def create_usd_receivable_account(self): account_name = "Debtors USD" if not frappe.db.get_value( diff --git a/erpnext/accounts/test/test_reports.py b/erpnext/accounts/test/test_reports.py index c2e10f8fd475..56b7832a32ec 100644 --- a/erpnext/accounts/test/test_reports.py +++ b/erpnext/accounts/test/test_reports.py @@ -14,8 +14,8 @@ REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [ ("General Ledger", {"group_by": "Group by Voucher (Consolidated)"}), ("General Ledger", {"group_by": "Group by Voucher (Consolidated)", "include_dimensions": 1}), - ("Accounts Payable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}), - ("Accounts Receivable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}), + ("Accounts Payable", {"range": "30, 60, 90, 120"}), + ("Accounts Receivable", {"range": "30, 60, 90, 120"}), ("Consolidated Financial Statement", {"report": "Balance Sheet"}), ("Consolidated Financial Statement", {"report": "Profit and Loss Statement"}), ("Consolidated Financial Statement", {"report": "Cash Flow"}), diff --git a/erpnext/accounts/test/test_utils.py b/erpnext/accounts/test/test_utils.py index 59cbc11794f4..5e108dee9b50 100644 --- a/erpnext/accounts/test/test_utils.py +++ b/erpnext/accounts/test/test_utils.py @@ -92,14 +92,14 @@ def test_update_reference_in_payment_entry(self): payment_entry.deductions = [] payment_entry.save() - # below is the difference between base_received_amount and base_paid_amount - self.assertEqual(payment_entry.difference_amount, -4855.0) + # below is the difference between base_paid_amount and base_received_amount (exchange gain) + self.assertEqual(payment_entry.deductions[0].amount, -4855.0) payment_entry.target_exchange_rate = 62.9 payment_entry.save() - # below is due to change in exchange rate - self.assertEqual(payment_entry.references[0].exchange_gain_loss, -4855.0) + # after changing the exchange rate, there is no exchange gain / loss + self.assertEqual(payment_entry.deductions, []) payment_entry.references = [] self.assertEqual(payment_entry.difference_amount, 0.0) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 0450221222d8..144039b794f7 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -474,10 +474,14 @@ def reconcile_against_document( doc = frappe.get_doc(voucher_type, voucher_no) frappe.flags.ignore_party_validation = True - # For payments with `Advance` in separate account feature enabled, only new ledger entries are posted for each reference. - # No need to cancel/delete payment ledger entries + # When Advance is allocated from an Order to an Invoice + # whole ledger must be reposted + repost_whole_ledger = any([x.voucher_detail_no for x in entries]) if voucher_type == "Payment Entry" and doc.book_advance_payments_in_separate_party_account: - doc.make_advance_gl_entries(cancel=1) + if repost_whole_ledger: + doc.make_gl_entries(cancel=1) + else: + doc.make_advance_gl_entries(cancel=1) else: _delete_pl_entries(voucher_type, voucher_no) @@ -511,9 +515,14 @@ def reconcile_against_document( doc = frappe.get_doc(entry.voucher_type, entry.voucher_no) if voucher_type == "Payment Entry" and doc.book_advance_payments_in_separate_party_account: - # both ledgers must be posted to for `Advance` in separate account feature - # TODO: find a more efficient way post only for the new linked vouchers - doc.make_advance_gl_entries() + # When Advance is allocated from an Order to an Invoice + # whole ledger must be reposted + if repost_whole_ledger: + doc.make_gl_entries() + else: + # both ledgers must be posted to for `Advance` in separate account feature + # TODO: find a more efficient way post only for the new linked vouchers + doc.make_advance_gl_entries() else: gl_map = doc.build_gl_map() # Make sure there is no overallocation @@ -621,6 +630,16 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False): if jv_detail.get("reference_type") in ["Sales Order", "Purchase Order"]: update_advance_paid.append((jv_detail.reference_type, jv_detail.reference_name)) + rev_dr_or_cr = ( + "debit_in_account_currency" + if d["dr_or_cr"] == "credit_in_account_currency" + else "credit_in_account_currency" + ) + if jv_detail.get(rev_dr_or_cr): + d["dr_or_cr"] = rev_dr_or_cr + d["allocated_amount"] = d["allocated_amount"] * -1 + d["unadjusted_amount"] = d["unadjusted_amount"] * -1 + if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0: # adjust the unreconciled balance amount_in_account_currency = flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) @@ -665,6 +684,8 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False): # will work as update after submit journal_entry.flags.ignore_validate_update_after_submit = True + # Ledgers will be reposted by Reconciliation tool + journal_entry.flags.ignore_reposting_on_reconciliation = True if not do_not_save: journal_entry.save(ignore_permissions=True) @@ -745,40 +766,114 @@ def cancel_exchange_gain_loss_journal( Cancel Exchange Gain/Loss for Sales/Purchase Invoice, if they have any. """ if parent_doc.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]: - journals = frappe.db.get_all( - "Journal Entry Account", - filters={ - "reference_type": parent_doc.doctype, - "reference_name": parent_doc.name, - "docstatus": 1, + gain_loss_journals = get_linked_exchange_gain_loss_journal( + referenced_dt=parent_doc.doctype, referenced_dn=parent_doc.name, je_docstatus=1 + ) + for doc in gain_loss_journals: + gain_loss_je = frappe.get_doc("Journal Entry", doc) + if referenced_dt and referenced_dn: + references = [(x.reference_type, x.reference_name) for x in gain_loss_je.accounts] + if ( + len(references) == 2 + and (referenced_dt, referenced_dn) in references + and (parent_doc.doctype, parent_doc.name) in references + ): + # only cancel JE generated against parent_doc and referenced_dn + gain_loss_je.cancel() + else: + gain_loss_je.cancel() + + +def delete_exchange_gain_loss_journal( + parent_doc: dict | object, referenced_dt: str | None = None, referenced_dn: str | None = None +) -> None: + """ + Delete Exchange Gain/Loss for Sales/Purchase Invoice, if they have any. + """ + if parent_doc.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]: + gain_loss_journals = get_linked_exchange_gain_loss_journal( + referenced_dt=parent_doc.doctype, referenced_dn=parent_doc.name, je_docstatus=2 + ) + for doc in gain_loss_journals: + gain_loss_je = frappe.get_doc("Journal Entry", doc) + if referenced_dt and referenced_dn: + references = [(x.reference_type, x.reference_name) for x in gain_loss_je.accounts] + if ( + len(references) == 2 + and (referenced_dt, referenced_dn) in references + and (parent_doc.doctype, parent_doc.name) in references + ): + # only delete JE generated against parent_doc and referenced_dn + gain_loss_je.delete() + else: + gain_loss_je.delete() + + +def get_linked_exchange_gain_loss_journal(referenced_dt: str, referenced_dn: str, je_docstatus: int) -> list: + """ + Get all the linked exchange gain/loss journal entries for a given document. + """ + gain_loss_journals = [] + if journals := frappe.db.get_all( + "Journal Entry Account", + { + "reference_type": referenced_dt, + "reference_name": referenced_dn, + "docstatus": je_docstatus, + }, + pluck="parent", + ): + gain_loss_journals = frappe.db.get_all( + "Journal Entry", + { + "name": ["in", journals], + "voucher_type": "Exchange Gain Or Loss", + "is_system_generated": 1, + "docstatus": je_docstatus, }, - fields=["parent"], - as_list=1, + pluck="name", ) + return gain_loss_journals - if journals: - gain_loss_journals = frappe.db.get_all( - "Journal Entry", - filters={ - "name": ["in", [x[0] for x in journals]], - "voucher_type": "Exchange Gain Or Loss", - "docstatus": 1, - }, - as_list=1, - ) - for doc in gain_loss_journals: - gain_loss_je = frappe.get_doc("Journal Entry", doc[0]) - if referenced_dt and referenced_dn: - references = [(x.reference_type, x.reference_name) for x in gain_loss_je.accounts] - if ( - len(references) == 2 - and (referenced_dt, referenced_dn) in references - and (parent_doc.doctype, parent_doc.name) in references - ): - # only cancel JE generated against parent_doc and referenced_dn - gain_loss_je.cancel() - else: - gain_loss_je.cancel() + +def cancel_common_party_journal(self): + if self.doctype not in ["Sales Invoice", "Purchase Invoice"]: + return + + if not frappe.db.get_single_value("Accounts Settings", "enable_common_party_accounting"): + return + + party_link = self.get_common_party_link() + if not party_link: + return + + journal_entry = frappe.db.get_value( + "Journal Entry Account", + filters={ + "reference_type": self.doctype, + "reference_name": self.name, + "docstatus": 1, + }, + fieldname="parent", + ) + + if not journal_entry: + return + + common_party_journal = frappe.db.get_value( + "Journal Entry", + filters={ + "name": journal_entry, + "is_system_generated": True, + "docstatus": 1, + }, + ) + + if not common_party_journal: + return + + common_party_je = frappe.get_doc("Journal Entry", common_party_journal) + common_party_je.cancel() def update_accounting_ledgers_after_reference_removal( @@ -1462,12 +1557,16 @@ def compare_existing_and_expected_gle(existing_gle, expected_gle, precision): return matched -def get_stock_accounts(company, voucher_type=None, voucher_no=None): +def get_stock_accounts(company, voucher_type=None, voucher_no=None, accounts=None): stock_accounts = [ d.name for d in frappe.db.get_all("Account", {"account_type": "Stock", "company": company, "is_group": 0}) ] - if voucher_type and voucher_no: + + if accounts: + stock_accounts = [row.account for row in accounts if row.account in stock_accounts] + + elif voucher_type and voucher_no: if voucher_type == "Journal Entry": stock_accounts = [ d.account @@ -1880,6 +1979,7 @@ def query_for_outstanding(self): ple.cost_center.as_("cost_center"), Sum(ple.amount).as_("amount"), Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"), + ple.remarks, ) .where(ple.delinked == 0) .where(Criterion.all(filter_on_voucher_no)) @@ -1942,6 +2042,7 @@ def query_for_outstanding(self): Table("vouchers").due_date, Table("vouchers").currency, Table("vouchers").cost_center.as_("cost_center"), + Table("vouchers").remarks, ) .where(Criterion.all(filter_on_outstanding_amount)) ) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 145e2ba3b3ea..21e307b480cf 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -213,7 +213,7 @@ frappe.ui.form.on("Asset", {
- Failed to post depreciation entries + ${__("Failed to post depreciation entries")}
`; @@ -416,7 +416,7 @@ frappe.ui.form.on("Asset", { } frm.dashboard.render_graph({ - title: "Asset Value", + title: __("Asset Value"), data: { labels: x_intervals, datasets: [ @@ -506,6 +506,7 @@ frappe.ui.form.on("Asset", { create_asset_repair: function (frm) { frappe.call({ args: { + company: frm.doc.company, asset: frm.doc.name, asset_name: frm.doc.asset_name, }, @@ -520,6 +521,7 @@ frappe.ui.form.on("Asset", { create_asset_capitalization: function (frm) { frappe.call({ args: { + company: frm.doc.company, asset: frm.doc.name, asset_name: frm.doc.asset_name, item_code: frm.doc.item_code, @@ -528,6 +530,7 @@ frappe.ui.form.on("Asset", { callback: function (r) { var doclist = frappe.model.sync(r.message); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); + $(".primary-action").prop("hidden", false); }, }); }, @@ -670,6 +673,11 @@ frappe.ui.form.on("Asset", { if (item.asset_location) { frm.set_value("location", item.asset_location); } + if (doctype === "Purchase Receipt") { + frm.set_value("purchase_receipt_item", item.name); + } else if (doctype === "Purchase Invoice") { + frm.set_value("purchase_invoice_item", item.name); + } }); }, diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index e408cc24d1eb..55932e9b7875 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -33,14 +33,16 @@ "dimension_col_break", "purchase_details_section", "purchase_receipt", + "purchase_receipt_item", "purchase_invoice", + "purchase_invoice_item", + "purchase_date", "available_for_use_date", - "total_asset_cost", - "additional_asset_cost", "column_break_23", "gross_purchase_amount", "asset_quantity", - "purchase_date", + "additional_asset_cost", + "total_asset_cost", "section_break_23", "calculate_depreciation", "column_break_33", @@ -536,6 +538,20 @@ "fieldname": "opening_number_of_booked_depreciations", "fieldtype": "Int", "label": "Opening Number of Booked Depreciations" + }, + { + "fieldname": "purchase_receipt_item", + "fieldtype": "Link", + "hidden": 1, + "label": "Purchase Receipt Item", + "options": "Purchase Receipt Item" + }, + { + "fieldname": "purchase_invoice_item", + "fieldtype": "Link", + "hidden": 1, + "label": "Purchase Invoice Item", + "options": "Purchase Invoice Item" } ], "idx": 72, @@ -579,7 +595,7 @@ "link_fieldname": "target_asset" } ], - "modified": "2024-08-01 16:39:09.340973", + "modified": "2024-08-26 23:28:29.095139", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 1b9e911be5b8..05d575ac8224 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -94,7 +94,9 @@ class Asset(AccountsController): purchase_amount: DF.Currency purchase_date: DF.Date | None purchase_invoice: DF.Link | None + purchase_invoice_item: DF.Link | None purchase_receipt: DF.Link | None + purchase_receipt_item: DF.Link | None split_from: DF.Link | None status: DF.Literal[ "Draft", @@ -117,13 +119,13 @@ class Asset(AccountsController): # end: auto-generated types def validate(self): + self.validate_precision() self.validate_asset_values() self.validate_asset_and_reference() self.validate_item() self.validate_cost_center() self.set_missing_values() self.validate_gross_and_purchase_amount() - self.validate_expected_value_after_useful_life() self.validate_finance_books() if not self.split_from: @@ -144,6 +146,7 @@ def validate(self): "Asset Depreciation Schedules created:
{0}

Please check, edit if needed, and submit the Asset." ).format(asset_depr_schedules_links) ) + self.validate_expected_value_after_useful_life() self.set_total_booked_depreciations() self.total_asset_cost = self.gross_purchase_amount self.status = self.get_status() @@ -304,6 +307,15 @@ def validate_finance_books(self): title=_("Missing Finance Book"), ) + def validate_precision(self): + float_precision = cint(frappe.db.get_default("float_precision")) or 2 + if self.gross_purchase_amount: + self.gross_purchase_amount = flt(self.gross_purchase_amount, float_precision) + if self.opening_accumulated_depreciation: + self.opening_accumulated_depreciation = flt( + self.opening_accumulated_depreciation, float_precision + ) + def validate_asset_values(self): if not self.asset_category: self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category") @@ -398,6 +410,9 @@ def set_depreciation_rate(self): ) def validate_asset_finance_books(self, row): + row.expected_value_after_useful_life = flt( + row.expected_value_after_useful_life, self.precision("gross_purchase_amount") + ) if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount): frappe.throw( _("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount").format( @@ -418,7 +433,10 @@ def validate_asset_finance_books(self, row): self.opening_accumulated_depreciation = 0 self.opening_number_of_booked_depreciations = 0 else: - depreciable_amount = flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life) + depreciable_amount = flt( + flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life), + self.precision("gross_purchase_amount"), + ) if flt(self.opening_accumulated_depreciation) > depreciable_amount: frappe.throw( _("Opening Accumulated Depreciation must be less than or equal to {0}").format( @@ -469,6 +487,9 @@ def set_total_booked_depreciations(self): def validate_expected_value_after_useful_life(self): for row in self.get("finance_books"): + row.expected_value_after_useful_life = flt( + row.expected_value_after_useful_life, self.precision("gross_purchase_amount") + ) depr_schedule = get_depr_schedule(self.name, "Draft", row.finance_book) if not depr_schedule: @@ -621,6 +642,9 @@ def get_manual_depreciation_entries(self): return records def validate_make_gl_entry(self): + if self.is_composite_asset: + return True + purchase_document = self.get_purchase_document() if not purchase_document: return False @@ -669,7 +693,7 @@ def get_fixed_asset_account(self): if not fixed_asset_account: frappe.throw( _("Set {0} in asset category {1} for company {2}").format( - frappe.bold("Fixed Asset Account"), + frappe.bold(_("Fixed Asset Account")), frappe.bold(self.asset_category), frappe.bold(self.company), ), @@ -691,12 +715,17 @@ def get_cwip_account(self, cwip_enabled=False): return cwip_account def make_gl_entries(self): + if self.check_asset_capitalization_gl_entries(): + return + gl_entries = [] purchase_document = self.get_purchase_document() fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account() - if purchase_document and self.purchase_amount and getdate(self.available_for_use_date) <= getdate(): + if (self.is_composite_asset or (purchase_document and self.purchase_amount)) and getdate( + self.available_for_use_date + ) <= getdate(): gl_entries.append( self.get_gl_dict( { @@ -733,6 +762,24 @@ def make_gl_entries(self): make_gl_entries(gl_entries) self.db_set("booked_fixed_asset", 1) + def check_asset_capitalization_gl_entries(self): + if self.is_composite_asset: + result = frappe.db.get_value( + "Asset Capitalization", + {"target_asset": self.name, "docstatus": 1}, + ["name", "target_fixed_asset_account"], + ) + + if result: + asset_capitalization, target_fixed_asset_account = result + # Check GL entries for the retrieved Asset Capitalization and target fixed asset account + return has_gl_entries( + "Asset Capitalization", asset_capitalization, target_fixed_asset_account + ) + # return if there are no submitted capitalization for given asset + return True + return False + @frappe.whitelist() def get_depreciation_rate(self, args, on_validate=False): if isinstance(args, str): @@ -762,14 +809,19 @@ def get_depreciation_rate(self, args, on_validate=False): args.get("value_after_depreciation") ) else: - value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount) + value = flt(args.get("expected_value_after_useful_life")) / ( + flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation) + ) depreciation_rate = math.pow( value, 1.0 / ( ( - flt(args.get("total_number_of_depreciations"), 2) + ( + flt(args.get("total_number_of_depreciations"), 2) + - flt(self.opening_number_of_booked_depreciations) + ) * flt(args.get("frequency_of_depreciation")) ) / 12 @@ -779,6 +831,22 @@ def get_depreciation_rate(self, args, on_validate=False): return flt((100 * (1 - depreciation_rate)), float_precision) +def has_gl_entries(doctype, docname, target_account): + gl_entry = frappe.qb.DocType("GL Entry") + gl_entries = ( + frappe.qb.from_(gl_entry) + .select(gl_entry.account) + .where( + (gl_entry.voucher_type == doctype) + & (gl_entry.voucher_no == docname) + & (gl_entry.debit != 0) + & (gl_entry.account == target_account) + ) + .run(as_dict=True) + ) + return len(gl_entries) > 0 + + def update_maintenance_status(): assets = frappe.get_all( "Asset", filters={"docstatus": 1, "maintenance_required": 1, "disposal_date": ("is", "not set")} @@ -854,18 +922,19 @@ def create_asset_maintenance(asset, item_code, item_name, asset_category, compan @frappe.whitelist() -def create_asset_repair(asset, asset_name): +def create_asset_repair(company, asset, asset_name): asset_repair = frappe.new_doc("Asset Repair") - asset_repair.update({"asset": asset, "asset_name": asset_name}) + asset_repair.update({"company": company, "asset": asset, "asset_name": asset_name}) return asset_repair @frappe.whitelist() -def create_asset_capitalization(asset, asset_name, item_code): +def create_asset_capitalization(company, asset, asset_name, item_code): asset_capitalization = frappe.new_doc("Asset Capitalization") asset_capitalization.update( { "target_asset": asset, + "company": company, "capitalization_method": "Choose a WIP composite asset", "target_asset_name": asset_name, "target_item_code": item_code, @@ -904,7 +973,7 @@ def transfer_asset(args): @frappe.whitelist() def get_item_details(item_code, asset_category, gross_purchase_amount): - asset_category_doc = frappe.get_doc("Asset Category", asset_category) + asset_category_doc = frappe.get_cached_doc("Asset Category", asset_category) books = [] for d in asset_category_doc.finance_books: books.append( diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 4c22f7ddd7a9..b38bd952bca7 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -317,7 +317,16 @@ def validate_source_mandatory(self): if not self.target_is_fixed_asset and not self.get("asset_items"): frappe.throw(_("Consumed Asset Items is mandatory for Decapitalization")) - if not (self.get("stock_items") or self.get("asset_items") or self.get("service_items")): + if self.capitalization_method == "Create a new composite asset" and not ( + self.get("stock_items") or self.get("asset_items") + ): + frappe.throw( + _( + "Consumed Stock Items or Consumed Asset Items are mandatory for creating new composite asset" + ) + ) + + elif not (self.get("stock_items") or self.get("asset_items") or self.get("service_items")): frappe.throw( _( "Consumed Stock Items, Consumed Asset Items or Consumed Service Items is mandatory for Capitalization" @@ -460,13 +469,24 @@ def get_gl_entries(self, warehouse_account=None, default_expense_account=None, d self.get_gl_entries_for_consumed_asset_items(gl_entries, target_account, target_against, precision) self.get_gl_entries_for_consumed_service_items(gl_entries, target_account, target_against, precision) - self.get_gl_entries_for_target_item(gl_entries, target_against, precision) + self.get_gl_entries_for_target_item(gl_entries, target_account, target_against, precision) return gl_entries def get_target_account(self): if self.target_is_fixed_asset: - return self.target_fixed_asset_account + from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled + + asset_category = frappe.get_cached_value("Asset", self.target_asset, "asset_category") + if is_cwip_accounting_enabled(asset_category): + target_account = get_asset_category_account( + "capital_work_in_progress_account", + asset_category=asset_category, + company=self.company, + ) + return target_account if target_account else self.target_fixed_asset_account + else: + return self.target_fixed_asset_account else: return self.warehouse_account[self.target_warehouse]["account"] @@ -554,13 +574,13 @@ def get_gl_entries_for_consumed_service_items( ) ) - def get_gl_entries_for_target_item(self, gl_entries, target_against, precision): + def get_gl_entries_for_target_item(self, gl_entries, target_account, target_against, precision): if self.target_is_fixed_asset: # Capitalization gl_entries.append( self.get_gl_dict( { - "account": self.target_fixed_asset_account, + "account": target_account, "against": ", ".join(target_against), "remarks": self.get("remarks") or _("Accounting Entry for Asset"), "debit": flt(self.total_value, precision), diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py index 5508bdcbef2a..86293fac765d 100644 --- a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py @@ -31,6 +31,12 @@ def setUp(self): def test_capitalization_with_perpetual_inventory(self): company = "_Test Company with perpetual inventory" set_depreciation_settings_in_company(company=company) + name = frappe.db.get_value( + "Asset Category Account", + filters={"parent": "Computers", "company_name": company}, + fieldname=["name"], + ) + frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", "") # Variables consumed_asset_value = 100000 @@ -187,9 +193,10 @@ def test_capitalization_with_periodical_inventory(self): # Test General Ledger Entries default_expense_account = frappe.db.get_value("Company", company, "default_expense_account") expected_gle = { - "_Test Fixed Asset - _TC": 3000, - "Expenses Included In Asset Valuation - _TC": -1000, - default_expense_account: -2000, + "_Test Fixed Asset - _TC": -100000.0, + default_expense_account: -2000.0, + "CWIP Account - _TC": 103000.0, + "Expenses Included In Asset Valuation - _TC": -1000.0, } actual_gle = get_actual_gle_dict(asset_capitalization.name) @@ -214,6 +221,12 @@ def test_capitalization_with_periodical_inventory(self): def test_capitalization_with_wip_composite_asset(self): company = "_Test Company with perpetual inventory" set_depreciation_settings_in_company(company=company) + name = frappe.db.get_value( + "Asset Category Account", + filters={"parent": "Computers", "company_name": company}, + fieldname=["name"], + ) + frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", "") stock_rate = 1000 stock_qty = 2 @@ -424,7 +437,7 @@ def test_capitalize_only_service_item(self): self.assertEqual(target_asset.purchase_amount, total_amount) expected_gle = { - "_Test Fixed Asset - _TC": 1000.0, + "CWIP Account - _TC": 1000.0, "Expenses Included In Asset Valuation - _TC": -1000.0, } diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py index bad89e93259a..679fbfe2e58d 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -767,8 +767,12 @@ def get_daily_depr_amount(asset, row, schedule_idx, amount): every_year_depr = amount / total_years + depr_period_start_date = add_days( + get_last_day(add_months(row.depreciation_start_date, row.frequency_of_depreciation * -1)), 1 + ) + year_start_date = add_years( - row.depreciation_start_date, (row.frequency_of_depreciation * schedule_idx) // 12 + depr_period_start_date, ((row.frequency_of_depreciation * schedule_idx) // 12) ) year_end_date = add_days(add_years(year_start_date, 1), -1) diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index b44164f2dae3..70ab7fdc8e84 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -144,6 +144,7 @@ def update_maintenance_log(asset_maintenance, item_code, item_name, task): "has_certificate": task.certificate_required, "description": task.description, "assign_to_name": task.assign_to_name, + "task_assignee_email": task.assign_to, "periodicity": str(task.periodicity), "maintenance_type": task.maintenance_type, "due_date": task.next_due_date, diff --git a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.json b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.json index 7d33176e2f37..c948630869b0 100644 --- a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.json +++ b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.json @@ -23,6 +23,7 @@ "column_break_6", "maintenance_status", "assign_to_name", + "task_assignee_email", "due_date", "completion_date", "description", @@ -168,15 +169,22 @@ "in_preview": 1, "label": "Task Name", "read_only": 1 + }, + { + "fieldname": "task_assignee_email", + "fieldtype": "Data", + "label": "Task Assignee Email", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-01-22 12:33:45.888124", + "modified": "2024-09-24 15:12:37.497853", "modified_by": "Administrator", "module": "Assets", "name": "Asset Maintenance Log", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -199,4 +207,4 @@ "sort_order": "DESC", "track_changes": 1, "track_seen": 1 -} \ No newline at end of file +} diff --git a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py index 95d02714c5b1..140eb1af27eb 100644 --- a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py +++ b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py @@ -37,6 +37,7 @@ class AssetMaintenanceLog(Document): naming_series: DF.Literal["ACC-AML-.YYYY.-"] periodicity: DF.Data | None task: DF.Link | None + task_assignee_email: DF.Data | None task_name: DF.Data | None # end: auto-generated types diff --git a/erpnext/assets/number_card/asset_value/asset_value.json b/erpnext/assets/number_card/asset_value/asset_value.json index 68e5f54c789c..c437f4800f35 100644 --- a/erpnext/assets/number_card/asset_value/asset_value.json +++ b/erpnext/assets/number_card/asset_value/asset_value.json @@ -4,6 +4,7 @@ "docstatus": 0, "doctype": "Number Card", "document_type": "Asset", + "dynamic_filters_json": "[[\"Asset\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", "filters_json": "[]", "function": "Sum", "idx": 0, diff --git a/erpnext/assets/number_card/new_assets_(this_year)/new_assets_(this_year).json b/erpnext/assets/number_card/new_assets_(this_year)/new_assets_(this_year).json index 6c8fb3565755..cdf52d011d4f 100644 --- a/erpnext/assets/number_card/new_assets_(this_year)/new_assets_(this_year).json +++ b/erpnext/assets/number_card/new_assets_(this_year)/new_assets_(this_year).json @@ -3,6 +3,7 @@ "docstatus": 0, "doctype": "Number Card", "document_type": "Asset", + "dynamic_filters_json": "[[\"Asset\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", "filters_json": "[[\"Asset\",\"creation\",\"Timespan\",\"this year\",false]]", "function": "Count", "idx": 0, diff --git a/erpnext/assets/number_card/total_assets/total_assets.json b/erpnext/assets/number_card/total_assets/total_assets.json index d127de8f2c64..d8626da608c3 100644 --- a/erpnext/assets/number_card/total_assets/total_assets.json +++ b/erpnext/assets/number_card/total_assets/total_assets.json @@ -3,6 +3,7 @@ "docstatus": 0, "doctype": "Number Card", "document_type": "Asset", + "dynamic_filters_json": "[[\"Asset\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", "filters_json": "[]", "function": "Count", "idx": 0, diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index bd92ebef3d7e..e71f24187940 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -65,10 +65,35 @@ frappe.ui.form.on("Purchase Order", { } }, + supplier: function (frm) { + // Do not update if inter company reference is there as the details will already be updated + if (frm.updating_party_details || frm.doc.inter_company_invoice_reference) return; + + if (frm.doc.__onload && frm.doc.__onload.load_after_mapping) return; + + erpnext.utils.get_party_details( + frm, + "erpnext.accounts.party.get_party_details", + { + posting_date: frm.doc.transaction_date, + bill_date: frm.doc.bill_date, + party: frm.doc.supplier, + party_type: "Supplier", + account: frm.doc.credit_to, + price_list: frm.doc.buying_price_list, + fetch_payment_terms_template: cint(!frm.doc.ignore_default_payment_terms_template), + }, + function () { + frm.set_df_property("apply_tds", "read_only", frm.supplier_tds ? 0 : 1); + frm.set_df_property("tax_withholding_category", "hidden", frm.supplier_tds ? 0 : 1); + } + ); + }, + get_materials_from_supplier: function (frm) { let po_details = []; - if (frm.doc.supplied_items && (flt(frm.doc.per_received, 2) == 100 || frm.doc.status === "Closed")) { + if (frm.doc.supplied_items && (flt(frm.doc.per_received) == 100 || frm.doc.status === "Closed")) { frm.doc.supplied_items.forEach((d) => { if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) { po_details.push(d.name); @@ -108,6 +133,15 @@ frappe.ui.form.on("Purchase Order", { frm.set_value("transaction_date", frappe.datetime.get_today()); } + if (frm.doc.__onload && frm.doc.supplier) { + if (frm.is_new()) { + frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0; + } + if (!frm.doc.__onload.supplier_tds) { + frm.set_df_property("apply_tds", "read_only", 1); + } + } + erpnext.queries.setup_queries(frm, "Warehouse", function () { return erpnext.queries.warehouse(frm.doc); }); @@ -295,8 +329,8 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends ( if (!["Closed", "Delivered"].includes(doc.status)) { if ( this.frm.doc.status !== "Closed" && - flt(this.frm.doc.per_received, 2) < 100 && - flt(this.frm.doc.per_billed, 2) < 100 + flt(this.frm.doc.per_received) < 100 && + flt(this.frm.doc.per_billed) < 100 ) { if (!this.frm.doc.__onload || this.frm.doc.__onload.can_update_items) { this.frm.add_custom_button(__("Update Items"), () => { @@ -310,7 +344,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends ( } } if (this.frm.has_perm("submit")) { - if (flt(doc.per_billed, 2) < 100 || flt(doc.per_received, 2) < 100) { + if (flt(doc.per_billed) < 100 || flt(doc.per_received) < 100) { if (doc.status != "On Hold") { this.frm.add_custom_button( __("Hold"), @@ -348,8 +382,8 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends ( } if (doc.status != "Closed") { if (doc.status != "On Hold") { - if (flt(doc.per_received, 2) < 100 && allow_receipt) { - cur_frm.add_custom_button( + if (flt(doc.per_received) < 100 && allow_receipt) { + this.frm.add_custom_button( __("Purchase Receipt"), this.make_purchase_receipt, __("Create") @@ -374,14 +408,15 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends ( } } } - if (flt(doc.per_billed, 2) < 100) - cur_frm.add_custom_button( + // Please do not add precision in the below flt function + if (flt(doc.per_billed) < 100) + this.frm.add_custom_button( __("Purchase Invoice"), this.make_purchase_invoice, __("Create") ); - if (flt(doc.per_billed, 2) < 100 && doc.status != "Delivered") { + if (flt(doc.per_billed) < 100 && doc.status != "Delivered") { this.frm.add_custom_button( __("Payment"), () => this.make_payment_entry(), @@ -389,7 +424,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends ( ); } - if (flt(doc.per_billed, 2) < 100) { + if (flt(doc.per_billed) < 100) { this.frm.add_custom_button( __("Payment Request"), function () { diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 0508483a0fc9..6ba9cb69f9fc 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -581,7 +581,7 @@ def update_reserved_qty_for_subcontract(self): def update_receiving_percentage(self): total_qty, received_qty = 0.0, 0.0 for item in self.items: - received_qty += item.received_qty + received_qty += min(item.received_qty, item.qty) total_qty += item.qty if total_qty: self.db_set("per_received", flt(received_qty / total_qty) * 100, update_modified=False) @@ -625,9 +625,11 @@ def update_ordered_qty_in_so_for_removed_items(self, removed_items): if not self.is_against_so(): return for item in removed_items: - prev_ordered_qty = frappe.get_cached_value( - "Sales Order Item", item.get("sales_order_item"), "ordered_qty" + prev_ordered_qty = ( + frappe.get_cached_value("Sales Order Item", item.get("sales_order_item"), "ordered_qty") + or 0.0 ) + frappe.db.set_value( "Sales Order Item", item.get("sales_order_item"), "ordered_qty", prev_ordered_qty - item.qty ) @@ -648,6 +650,13 @@ def update_subcontracting_order_status(self): if sco: update_sco_status(sco, "Closed" if self.status == "Closed" else None) + def set_missing_values(self, for_validate=False): + tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category") + if tds_category and not for_validate: + self.set_onload("supplier_tds", tds_category) + + super().set_missing_values(for_validate) + @frappe.request_cache def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0): @@ -731,7 +740,7 @@ def update_item(obj, target, source_parent): "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) and doc.delivered_by_supplier != 1, }, - "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "add_if_empty": True}, + "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True}, }, target_doc, set_missing_values, @@ -760,6 +769,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions def postprocess(source, target): target.flags.ignore_permissions = ignore_permissions set_missing_values(source, target) + + # set tax_withholding_category from Purchase Order + if source.apply_tds and source.tax_withholding_category and target.apply_tds: + target.tax_withholding_category = source.tax_withholding_category + # Get the advance paid Journal Entries in Purchase Invoice Advance if target.get("allocate_advances_automatically"): target.set_advances() @@ -807,7 +821,7 @@ def update_item(obj, target, source_parent): "postprocess": update_item, "condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)), }, - "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "add_if_empty": True}, + "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True}, } doc = get_mapped_doc( @@ -877,6 +891,20 @@ def make_subcontracting_order(source_name, target_doc=None, save=False, submit=F def get_mapped_subcontracting_order(source_name, target_doc=None): + def post_process(source_doc, target_doc): + target_doc.populate_items_table() + + if target_doc.set_warehouse: + for item in target_doc.items: + item.warehouse = target_doc.set_warehouse + else: + if source_doc.set_warehouse: + for item in target_doc.items: + item.warehouse = source_doc.set_warehouse + else: + for idx, item in enumerate(target_doc.items): + item.warehouse = source_doc.items[idx].warehouse + if target_doc and isinstance(target_doc, str): target_doc = json.loads(target_doc) for key in ["service_items", "items", "supplied_items"]: @@ -907,22 +935,9 @@ def get_mapped_subcontracting_order(source_name, target_doc=None): }, }, target_doc, + post_process, ) - target_doc.populate_items_table() - source_doc = frappe.get_doc("Purchase Order", source_name) - - if target_doc.set_warehouse: - for item in target_doc.items: - item.warehouse = target_doc.set_warehouse - else: - if source_doc.set_warehouse: - for item in target_doc.items: - item.warehouse = source_doc.set_warehouse - else: - for idx, item in enumerate(target_doc.items): - item.warehouse = source_doc.items[idx].warehouse - return target_doc diff --git a/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py b/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py index 36fe079fc98d..3fb8b30f139d 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py @@ -14,18 +14,25 @@ def get_data(): "Material Request": ["items", "material_request"], "Supplier Quotation": ["items", "supplier_quotation"], "Project": ["items", "project"], + "Sales Order": ["items", "sales_order"], + "BOM": ["items", "bom"], + "Production Plan": ["items", "production_plan"], + "Blanket Order": ["items", "blanket_order"], }, "transactions": [ - {"label": _("Related"), "items": ["Purchase Receipt", "Purchase Invoice"]}, + {"label": _("Related"), "items": ["Purchase Receipt", "Purchase Invoice", "Sales Order"]}, {"label": _("Payment"), "items": ["Payment Entry", "Journal Entry", "Payment Request"]}, { "label": _("Reference"), - "items": ["Material Request", "Supplier Quotation", "Project", "Auto Repeat"], + "items": ["Supplier Quotation", "Project", "Auto Repeat"], + }, + { + "label": _("Manufacturing"), + "items": ["Material Request", "BOM", "Production Plan", "Blanket Order"], }, { "label": _("Sub-contracting"), "items": ["Subcontracting Order", "Subcontracting Receipt", "Stock Entry"], }, - {"label": _("Internal"), "items": ["Sales Order"]}, ], } diff --git a/erpnext/buying/doctype/purchase_order/purchase_order_list.js b/erpnext/buying/doctype/purchase_order/purchase_order_list.js index c1bf1f3b8d95..7b37987b926b 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order_list.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order_list.js @@ -10,14 +10,15 @@ frappe.listview_settings["Purchase Order"] = { "status", ], get_indicator: function (doc) { + // Please do not add precision in the flt function if (doc.status === "Closed") { return [__("Closed"), "green", "status,=,Closed"]; } else if (doc.status === "On Hold") { return [__("On Hold"), "orange", "status,=,On Hold"]; } else if (doc.status === "Delivered") { return [__("Delivered"), "green", "status,=,Closed"]; - } else if (flt(doc.per_received, 2) < 100 && doc.status !== "Closed") { - if (flt(doc.per_billed, 2) < 100) { + } else if (flt(doc.per_received) < 100 && doc.status !== "Closed") { + if (flt(doc.per_billed) < 100) { return [ __("To Receive and Bill"), "orange", @@ -26,17 +27,9 @@ frappe.listview_settings["Purchase Order"] = { } else { return [__("To Receive"), "orange", "per_received,<,100|per_billed,=,100|status,!=,Closed"]; } - } else if ( - flt(doc.per_received, 2) >= 100 && - flt(doc.per_billed, 2) < 100 && - doc.status !== "Closed" - ) { + } else if (flt(doc.per_received) >= 100 && flt(doc.per_billed) < 100 && doc.status !== "Closed") { return [__("To Bill"), "orange", "per_received,=,100|per_billed,<,100|status,!=,Closed"]; - } else if ( - flt(doc.per_received, 2) >= 100 && - flt(doc.per_billed, 2) == 100 && - doc.status !== "Closed" - ) { + } else if (flt(doc.per_received) >= 100 && flt(doc.per_billed) == 100 && doc.status !== "Closed") { return [__("Completed"), "green", "per_received,=,100|per_billed,=,100|status,!=,Closed"]; } }, diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index b8689d29a569..d7c2c3f24b1d 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -146,8 +146,8 @@ frappe.ui.form.on("Request for Quotation", { return; } }, - "Download PDF for Supplier", - "Download" + __("Download PDF for Supplier"), + __("Download") ); }, __("Tools") @@ -272,9 +272,10 @@ frappe.ui.form.on("Request for Quotation", { }); }; - dialog.fields_dict.note.$wrapper - .append(`

This is a preview of the email to be sent. A PDF of the document will - automatically be attached with the email.

`); + const msg = __( + "This is a preview of the email to be sent. A PDF of the document will automatically be attached with the email." + ); + dialog.fields_dict.note.$wrapper.append(`

${msg}

`); dialog.show(); }, diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index c8ef833e0e97..3a71733a0039 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -390,6 +390,7 @@ def postprocess(source, target_doc): "Request for Quotation": { "doctype": "Supplier Quotation", "validation": {"docstatus": ["=", 1]}, + "field_map": {"opportunity": "opportunity"}, }, "Request for Quotation Item": { "doctype": "Supplier Quotation Item", @@ -455,6 +456,7 @@ def create_rfq_items(sq_doc, supplier, data): "material_request", "material_request_item", "stock_qty", + "uom", ]: args[field] = data.get(field) diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index bccab8b01e07..3b72953c5636 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -97,7 +97,7 @@ def autoname(self): elif supp_master_name == "Naming Series": set_name_by_naming_series(self) else: - self.name = set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self) + set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self) def on_update(self): self.create_primary_contact() diff --git a/erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.js b/erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.js new file mode 100644 index 000000000000..37870b43b6db --- /dev/null +++ b/erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.js @@ -0,0 +1,62 @@ +// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.query_reports["Item-wise Purchase History"] = { + filters: [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, + }, + { + fieldname: "from_date", + reqd: 1, + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + }, + { + fieldname: "to_date", + reqd: 1, + default: frappe.datetime.get_today(), + label: __("To Date"), + fieldtype: "Date", + }, + { + fieldname: "item_group", + label: __("Item Group"), + fieldtype: "Link", + options: "Item Group", + }, + { + fieldname: "item_code", + label: __("Item"), + fieldtype: "Link", + options: "Item", + get_query: () => { + return { + query: "erpnext.controllers.queries.item_query", + }; + }, + }, + { + fieldname: "supplier", + label: __("Supplier"), + fieldtype: "Link", + options: "Supplier", + }, + ], + + formatter: function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + let format_fields = ["received_qty", "billed_amt"]; + + if (format_fields.includes(column.fieldname) && data && data[column.fieldname] > 0) { + value = "" + value + ""; + } + return value; + }, +}; diff --git a/erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.json b/erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.json index 521c68c53298..35045afcf8b9 100644 --- a/erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.json +++ b/erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.json @@ -1,30 +1,30 @@ { - "add_total_row": 1, - "apply_user_permissions": 1, - "creation": "2013-05-03 14:55:53", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2017-02-24 20:08:57.446613", - "modified_by": "Administrator", - "module": "Buying", - "name": "Item-wise Purchase History", - "owner": "Administrator", - "query": "select\n po_item.item_code as \"Item Code:Link/Item:120\",\n\tpo_item.item_name as \"Item Name::120\",\n po_item.item_group as \"Item Group:Link/Item Group:120\",\n\tpo_item.description as \"Description::150\",\n\tpo_item.qty as \"Qty:Float:100\",\n\tpo_item.uom as \"UOM:Link/UOM:80\",\n\tpo_item.base_rate as \"Rate:Currency:120\",\n\tpo_item.base_amount as \"Amount:Currency:120\",\n\tpo.name as \"Purchase Order:Link/Purchase Order:120\",\n\tpo.transaction_date as \"Transaction Date:Date:140\",\n\tpo.supplier as \"Supplier:Link/Supplier:130\",\n sup.supplier_name as \"Supplier Name::150\",\n\tpo_item.project as \"Project:Link/Project:130\",\n\tifnull(po_item.received_qty, 0) as \"Received Qty:Float:120\",\n\tpo.company as \"Company:Link/Company:\"\nfrom\n\t`tabPurchase Order` po, `tabPurchase Order Item` po_item, `tabSupplier` sup\nwhere\n\tpo.name = po_item.parent and po.supplier = sup.name and po.docstatus = 1\norder by po.name desc", - "ref_doctype": "Purchase Order", - "report_name": "Item-wise Purchase History", - "report_type": "Query Report", + "add_total_row": 1, + "creation": "2013-05-03 14:55:53", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 5, + "is_standard": "Yes", + "modified": "2024-06-19 12:12:15.418799", + "modified_by": "Administrator", + "module": "Buying", + "name": "Item-wise Purchase History", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Purchase Order", + "report_name": "Item-wise Purchase History", + "report_type": "Script Report", "roles": [ { "role": "Stock User" - }, + }, { "role": "Purchase Manager" - }, + }, { "role": "Purchase User" } - ] + ] } \ No newline at end of file diff --git a/erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.py b/erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.py new file mode 100644 index 000000000000..a8950af3ea37 --- /dev/null +++ b/erpnext/buying/report/item_wise_purchase_history/item_wise_purchase_history.py @@ -0,0 +1,276 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.utils import flt +from frappe.utils.nestedset import get_descendants_of + + +def execute(filters=None): + filters = frappe._dict(filters or {}) + if filters.from_date > filters.to_date: + frappe.throw(_("From Date cannot be greater than To Date")) + + columns = get_columns(filters) + data = get_data(filters) + + chart_data = get_chart_data(data) + + return columns, data, None, chart_data + + +def get_columns(filters): + return [ + { + "label": _("Item Code"), + "fieldtype": "Link", + "fieldname": "item_code", + "options": "Item", + "width": 120, + }, + { + "label": _("Item Name"), + "fieldtype": "Data", + "fieldname": "item_name", + "width": 140, + }, + { + "label": _("Item Group"), + "fieldtype": "Link", + "fieldname": "item_group", + "options": "Item Group", + "width": 120, + }, + { + "label": _("Description"), + "fieldtype": "Data", + "fieldname": "description", + "width": 140, + }, + { + "label": _("Quantity"), + "fieldtype": "Float", + "fieldname": "quantity", + "width": 120, + }, + { + "label": _("UOM"), + "fieldtype": "Link", + "fieldname": "uom", + "options": "UOM", + "width": 90, + }, + { + "label": _("Rate"), + "fieldname": "rate", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Amount"), + "fieldname": "amount", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "label": _("Purchase Order"), + "fieldtype": "Link", + "fieldname": "purchase_order", + "options": "Purchase Order", + "width": 160, + }, + { + "label": _("Transaction Date"), + "fieldtype": "Date", + "fieldname": "transaction_date", + "width": 110, + }, + { + "label": _("Supplier"), + "fieldtype": "Link", + "fieldname": "supplier", + "options": "Supplier", + "width": 100, + }, + { + "label": _("Supplier Name"), + "fieldtype": "Data", + "fieldname": "supplier_name", + "width": 140, + }, + { + "label": _("Supplier Group"), + "fieldtype": "Link", + "fieldname": "supplier_group", + "options": "Supplier Group", + "width": 120, + }, + { + "label": _("Project"), + "fieldtype": "Link", + "fieldname": "project", + "options": "Project", + "width": 100, + }, + { + "label": _("Received Quantity"), + "fieldtype": "Float", + "fieldname": "received_qty", + "width": 150, + }, + { + "label": _("Billed Amount"), + "fieldtype": "Currency", + "fieldname": "billed_amt", + "options": "currency", + "width": 120, + }, + { + "label": _("Company"), + "fieldtype": "Link", + "fieldname": "company", + "options": "Company", + "width": 100, + }, + { + "label": _("Currency"), + "fieldtype": "Link", + "fieldname": "currency", + "options": "Currency", + "hidden": 1, + }, + ] + + +def get_data(filters): + data = [] + + company_list = get_descendants_of("Company", filters.get("company")) + company_list.append(filters.get("company")) + + supplier_details = get_supplier_details() + item_details = get_item_details() + purchase_order_records = get_purchase_order_details(company_list, filters) + + for record in purchase_order_records: + supplier_record = supplier_details.get(record.supplier) + item_record = item_details.get(record.item_code) + row = { + "item_code": record.get("item_code"), + "item_name": item_record.get("item_name"), + "item_group": item_record.get("item_group"), + "description": record.get("description"), + "quantity": record.get("qty"), + "uom": record.get("uom"), + "rate": record.get("base_rate"), + "amount": record.get("base_amount"), + "purchase_order": record.get("name"), + "transaction_date": record.get("transaction_date"), + "supplier": record.get("supplier"), + "supplier_name": supplier_record.get("supplier_name"), + "supplier_group": supplier_record.get("supplier_group"), + "project": record.get("project"), + "received_qty": flt(record.get("received_qty")), + "billed_amt": flt(record.get("billed_amt")), + "company": record.get("company"), + } + row["currency"] = frappe.get_cached_value("Company", row["company"], "default_currency") + data.append(row) + + return data + + +def get_supplier_details(): + details = frappe.get_all("Supplier", fields=["name", "supplier_name", "supplier_group"]) + supplier_details = {} + for d in details: + supplier_details.setdefault( + d.name, + frappe._dict({"supplier_name": d.supplier_name, "supplier_group": d.supplier_group}), + ) + return supplier_details + + +def get_item_details(): + details = frappe.db.get_all("Item", fields=["name", "item_name", "item_group"]) + item_details = {} + for d in details: + item_details.setdefault(d.name, frappe._dict({"item_name": d.item_name, "item_group": d.item_group})) + return item_details + + +def get_purchase_order_details(company_list, filters): + db_po = frappe.qb.DocType("Purchase Order") + db_po_item = frappe.qb.DocType("Purchase Order Item") + + query = ( + frappe.qb.from_(db_po) + .inner_join(db_po_item) + .on(db_po_item.parent == db_po.name) + .select( + db_po.name, + db_po.supplier, + db_po.transaction_date, + db_po.project, + db_po.company, + db_po_item.item_code, + db_po_item.description, + db_po_item.qty, + db_po_item.uom, + db_po_item.base_rate, + db_po_item.base_amount, + db_po_item.received_qty, + (db_po_item.billed_amt * db_po.conversion_rate).as_("billed_amt"), + ) + .where(db_po.docstatus == 1) + .where(db_po.company.isin(tuple(company_list))) + ) + + for field in ("item_code", "item_group"): + if filters.get(field): + query = query.where(db_po_item[field] == filters[field]) + + if filters.get("from_date"): + query = query.where(db_po.transaction_date >= filters.from_date) + + if filters.get("to_date"): + query = query.where(db_po.transaction_date <= filters.to_date) + + if filters.get("supplier"): + query = query.where(db_po.supplier == filters.supplier) + + return query.run(as_dict=1) + + +def get_chart_data(data): + item_wise_purchase_map = {} + labels, datapoints = [], [] + + for row in data: + item_key = row.get("item_code") + + if item_key not in item_wise_purchase_map: + item_wise_purchase_map[item_key] = 0 + + item_wise_purchase_map[item_key] = flt(item_wise_purchase_map[item_key]) + flt(row.get("amount")) + + item_wise_purchase_map = { + item: value + for item, value in (sorted(item_wise_purchase_map.items(), key=lambda i: i[1], reverse=True)) + } + + for key in item_wise_purchase_map: + labels.append(key) + datapoints.append(item_wise_purchase_map[key]) + + return { + "data": { + "labels": labels[:30], # show max of 30 items in chart + "datasets": [{"name": _("Total Purchase Amount"), "values": datapoints[:30]}], + }, + "type": "bar", + "fieldtype": "Currency", + } diff --git a/erpnext/buying/report/procurement_tracker/procurement_tracker.py b/erpnext/buying/report/procurement_tracker/procurement_tracker.py index a7e03c08face..bd0798236b30 100644 --- a/erpnext/buying/report/procurement_tracker/procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/procurement_tracker.py @@ -175,7 +175,7 @@ def get_data(filters): "purchase_order": po.parent, "supplier": po.supplier, "estimated_cost": flt(mr_record.get("amount")), - "actual_cost": flt(pi_records.get(po.name)), + "actual_cost": flt(pi_records.get(po.name)) or flt(po.amount), "purchase_order_amt": flt(po.amount), "purchase_order_amt_in_company_currency": flt(po.base_amount), "expected_delivery_date": po.schedule_date, diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py index da1c70d31793..6d2034d18789 100644 --- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py +++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py @@ -18,6 +18,7 @@ def execute(filters=None): columns = get_columns(filters) data = get_data(filters) + update_received_amount(data) if not data: return [], [], None, [] @@ -60,7 +61,6 @@ def get_data(filters): (po_item.qty - po_item.received_qty).as_("pending_qty"), Sum(IfNull(pi_item.qty, 0)).as_("billed_qty"), po_item.base_amount.as_("amount"), - (po_item.received_qty * po_item.base_rate).as_("received_qty_amount"), (po_item.billed_amt * IfNull(po.conversion_rate, 1)).as_("billed_amount"), (po_item.base_amount - (po_item.billed_amt * IfNull(po.conversion_rate, 1))).as_( "pending_amount" @@ -92,6 +92,39 @@ def get_data(filters): return data +def update_received_amount(data): + pr_data = get_received_amount_data(data) + + for row in data: + row.received_qty_amount = flt(pr_data.get(row.name)) + + +def get_received_amount_data(data): + pr = frappe.qb.DocType("Purchase Receipt") + pr_item = frappe.qb.DocType("Purchase Receipt Item") + + query = ( + frappe.qb.from_(pr) + .inner_join(pr_item) + .on(pr_item.parent == pr.name) + .select( + pr_item.purchase_order_item, + Sum(pr_item.base_amount).as_("received_qty_amount"), + ) + .where((pr_item.parent == pr.name) & (pr.docstatus == 1)) + .groupby(pr_item.purchase_order_item) + ) + + query = query.where(pr_item.purchase_order_item.isin([row.name for row in data])) + + data = query.run() + + if not data: + return frappe._dict() + + return frappe._dict(data) + + def prepare_data(data, filters): completed, pending = 0, 0 pending_field = "pending_amount" @@ -147,7 +180,7 @@ def prepare_data(data, filters): def prepare_chart_data(pending, completed): - labels = ["Amount to Bill", "Billed Amount"] + labels = [_("Amount to Bill"), _("Billed Amount")] return { "data": {"labels": labels, "datasets": [{"values": [pending, completed]}]}, diff --git a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.js b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.js index 56684a8659bf..9b193a34d838 100644 --- a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.js +++ b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.js @@ -2,3 +2,10 @@ // License: GNU General Public License v3. See license.txt frappe.query_reports["Purchase Order Trends"] = $.extend({}, erpnext.purchase_trends_filters); + +frappe.query_reports["Purchase Order Trends"]["filters"].push({ + fieldname: "include_closed_orders", + label: __("Include Closed Orders"), + fieldtype: "Check", + default: 0, +}); diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 63d0404642a3..e1a7d5803c9a 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -233,7 +233,7 @@ def validate(self): ).format( frappe.bold(document_type), get_link_to_form(self.doctype, self.get("return_against")), - frappe.bold("Update Outstanding for Self"), + frappe.bold(_("Update Outstanding for Self")), get_link_to_form("Payment Reconciliation", "Payment Reconciliation"), ) ) @@ -345,13 +345,30 @@ def _remove_references_in_repost_doctypes(self): repost_doc.flags.ignore_links = True repost_doc.save(ignore_permissions=True) + def _remove_advance_payment_ledger_entries(self): + adv = qb.DocType("Advance Payment Ledger Entry") + qb.from_(adv).delete().where(adv.voucher_type.eq(self.doctype) & adv.voucher_no.eq(self.name)).run() + + advance_payment_doctypes = frappe.get_hooks("advance_payment_doctypes") + + if self.doctype in advance_payment_doctypes: + qb.from_(adv).delete().where( + adv.against_voucher_type.eq(self.doctype) & adv.against_voucher_no.eq(self.name) + ).run() + def on_trash(self): + from erpnext.accounts.utils import delete_exchange_gain_loss_journal + + self._remove_advance_payment_ledger_entries() self._remove_references_in_repost_doctypes() self._remove_references_in_unreconcile() self.remove_serial_and_batch_bundle() # delete sl and gl entries on deletion of transaction if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"): + # delete linked exchange gain/loss journal + delete_exchange_gain_loss_journal(self) + ple = frappe.qb.DocType("Payment Ledger Entry") frappe.qb.from_(ple).delete().where( (ple.voucher_type == self.doctype) & (ple.voucher_no == self.name) @@ -388,12 +405,15 @@ def remove_serial_and_batch_bundle(self): def validate_return_against_account(self): if self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.is_return and self.return_against: cr_dr_account_field = "debit_to" if self.doctype == "Sales Invoice" else "credit_to" - cr_dr_account_label = "Debit To" if self.doctype == "Sales Invoice" else "Credit To" - cr_dr_account = self.get(cr_dr_account_field) - if frappe.get_value(self.doctype, self.return_against, cr_dr_account_field) != cr_dr_account: + original_account = frappe.get_value(self.doctype, self.return_against, cr_dr_account_field) + if original_account != self.get(cr_dr_account_field): frappe.throw( - _("'{0}' account: '{1}' should match the Return Against Invoice").format( - frappe.bold(cr_dr_account_label), frappe.bold(cr_dr_account) + _( + "Please set {0} to {1}, the same account that was used in the original invoice {2}." + ).format( + frappe.bold(_(self.meta.get_label(cr_dr_account_field), context=self.doctype)), + frappe.bold(original_account), + frappe.bold(self.return_against), ) ) @@ -443,6 +463,11 @@ def validate_deferred_start_and_end_date(self): ) def validate_invoice_documents_schedule(self): + if self.is_return: + self.payment_terms_template = "" + self.payment_schedule = [] + return + self.validate_payment_schedule_dates() self.set_due_date() self.set_payment_schedule() @@ -457,7 +482,7 @@ def validate_non_invoice_documents_schedule(self): self.validate_payment_schedule_amount() def validate_all_documents_schedule(self): - if self.doctype in ("Sales Invoice", "Purchase Invoice") and not self.is_return: + if self.doctype in ("Sales Invoice", "Purchase Invoice"): self.validate_invoice_documents_schedule() elif self.doctype in ("Quotation", "Purchase Order", "Sales Order"): self.validate_non_invoice_documents_schedule() @@ -1031,7 +1056,9 @@ def get_gl_dict(self, args, account_currency=None, item=None): gl_dict.update( { "transaction_currency": self.get("currency") or self.company_currency, - "transaction_exchange_rate": self.get("conversion_rate", 1), + "transaction_exchange_rate": item.get("exchange_rate", 1) + if self.doctype == "Journal Entry" and item + else self.get("conversion_rate", 1), "debit_in_transaction_currency": self.get_value_in_transaction_currency( account_currency, gl_dict, "debit" ), @@ -1056,16 +1083,26 @@ def get_voucher_subtype(self): "Stock Entry": "stock_entry_type", "Asset Capitalization": "entry_type", } + + for method_name in frappe.get_hooks("voucher_subtypes"): + voucher_subtype = frappe.get_attr(method_name)(self) + + if voucher_subtype: + return voucher_subtype + if self.doctype in voucher_subtypes: return self.get(voucher_subtypes[self.doctype]) elif self.doctype == "Purchase Receipt" and self.is_return: return "Purchase Return" elif self.doctype == "Delivery Note" and self.is_return: return "Sales Return" - elif (self.doctype == "Sales Invoice" and self.is_return) or self.doctype == "Purchase Invoice": + elif self.doctype == "Sales Invoice" and self.is_return: return "Credit Note" - elif (self.doctype == "Purchase Invoice" and self.is_return) or self.doctype == "Sales Invoice": + elif self.doctype == "Sales Invoice" and self.is_debit_note: + return "Debit Note" + elif self.doctype == "Purchase Invoice" and self.is_return: return "Debit Note" + return self.doctype def get_value_in_transaction_currency(self, account_currency, gl_dict, field): @@ -1261,7 +1298,11 @@ def set_advance_gain_or_loss(self): d.exchange_gain_loss = difference def make_precision_loss_gl_entry(self, gl_entries): - round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( + ( + round_off_account, + round_off_cost_center, + round_off_for_opening, + ) = get_round_off_account_and_cost_center( self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center ) @@ -1334,6 +1375,12 @@ def make_exchange_gain_loss_journal( # Cancelling existing exchange gain/loss journals is handled during the `on_cancel` event. # see accounts/utils.py:cancel_exchange_gain_loss_journal() if self.docstatus == 1: + if dimensions_dict is None: + dimensions_dict = frappe._dict() + active_dimensions = get_dimensions()[0] + for dim in active_dimensions: + dimensions_dict[dim.fieldname] = self.get(dim.fieldname) + if self.get("doctype") == "Journal Entry": # 'args' is populated with exchange gain/loss account and the amount to be booked. # These are generated by Sales/Purchase Invoice during reconciliation and advance allocation. @@ -1574,6 +1621,7 @@ def on_cancel(self): remove_from_bank_transaction, ) from erpnext.accounts.utils import ( + cancel_common_party_journal, cancel_exchange_gain_loss_journal, unlink_ref_doc_from_payment_entries, ) @@ -1585,6 +1633,7 @@ def on_cancel(self): # Cancel Exchange Gain/Loss Journal before unlinking cancel_exchange_gain_loss_journal(self) + cancel_common_party_journal(self) if frappe.db.get_single_value("Accounts Settings", "unlink_payment_on_cancellation_of_invoice"): unlink_ref_doc_from_payment_entries(self) @@ -1904,21 +1953,23 @@ def get_stock_items(self): return stock_items - def set_total_advance_paid(self): - ple = frappe.qb.DocType("Payment Ledger Entry") - party = self.customer if self.doctype == "Sales Order" else self.supplier + def calculate_total_advance_from_ledger(self): + adv = frappe.qb.DocType("Advance Payment Ledger Entry") advance = ( - frappe.qb.from_(ple) - .select(ple.account_currency, Abs(Sum(ple.amount_in_account_currency)).as_("amount")) + frappe.qb.from_(adv) + .select(adv.currency.as_("account_currency"), Abs(Sum(adv.amount)).as_("amount")) .where( - (ple.against_voucher_type == self.doctype) - & (ple.against_voucher_no == self.name) - & (ple.party == party) - & (ple.delinked == 0) - & (ple.company == self.company) + (adv.against_voucher_type == self.doctype) + & (adv.against_voucher_no == self.name) + & (adv.company == self.company) ) .run(as_dict=True) ) + return advance + + def set_total_advance_paid(self): + advance = self.calculate_total_advance_from_ledger() + advance_paid, order_total = None, None if advance: advance = advance[0] @@ -1951,7 +2002,7 @@ def set_total_advance_paid(self): ).format(formatted_advance_paid, self.name, formatted_order_total) ) - frappe.db.set_value(self.doctype, self.name, "advance_paid", advance_paid) + self.db_set("advance_paid", advance_paid) @property def company_abbr(self): @@ -1962,7 +2013,9 @@ def company_abbr(self): def raise_missing_debit_credit_account_error(self, party_type, party): """Raise an error if debit to/credit to account does not exist.""" - db_or_cr = frappe.bold("Debit To") if self.doctype == "Sales Invoice" else frappe.bold("Credit To") + db_or_cr = ( + frappe.bold(_("Debit To")) if self.doctype == "Sales Invoice" else frappe.bold(_("Credit To")) + ) rec_or_pay = "Receivable" if self.doctype == "Sales Invoice" else "Payable" link_to_party = frappe.utils.get_link_to_form(party_type, party) @@ -2410,12 +2463,21 @@ def create_advance_and_reconcile(self, party_link): primary_account = get_party_account(primary_party_type, primary_party, self.company) secondary_account = get_party_account(secondary_party_type, secondary_party, self.company) + primary_account_currency = get_account_currency(primary_account) + secondary_account_currency = get_account_currency(secondary_account) + default_currency = erpnext.get_company_currency(self.company) + + # Determine if multi-currency journal entry is needed + multi_currency = ( + primary_account_currency != default_currency or secondary_account_currency != default_currency + ) jv = frappe.new_doc("Journal Entry") jv.voucher_type = "Journal Entry" jv.posting_date = self.posting_date jv.company = self.company jv.remark = f"Adjustment for {self.doctype} {self.name}" + jv.is_system_generated = True reconcilation_entry = frappe._dict() advance_entry = frappe._dict() @@ -2433,7 +2495,7 @@ def create_advance_and_reconcile(self, party_link): advance_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(self.company) advance_entry.is_advance = "Yes" - # update dimesions + # Update dimensions dimensions_dict = frappe._dict() active_dimensions = get_dimensions()[0] for dim in active_dimensions: @@ -2442,13 +2504,58 @@ def create_advance_and_reconcile(self, party_link): reconcilation_entry.update(dimensions_dict) advance_entry.update(dimensions_dict) - if self.doctype == "Sales Invoice": - reconcilation_entry.credit_in_account_currency = self.outstanding_amount - advance_entry.debit_in_account_currency = self.outstanding_amount + # Calculate exchange rates if necessary + if multi_currency: + # Exchange rates for primary and secondary accounts + exc_rate_primary_to_default = ( + 1 + if primary_account_currency == default_currency + else get_exchange_rate(primary_account_currency, default_currency, self.posting_date) + ) + exc_rate_secondary_to_default = ( + 1 + if secondary_account_currency == default_currency + else get_exchange_rate(secondary_account_currency, default_currency, self.posting_date) + ) + exc_rate_secondary_to_primary = ( + 1 + if secondary_account_currency == primary_account_currency + else get_exchange_rate( + secondary_account_currency, primary_account_currency, self.posting_date + ) + ) + + # Convert outstanding amount from secondary to primary account currency, if needed + + os_in_default_currency = self.outstanding_amount * exc_rate_secondary_to_default + os_in_primary_currency = self.outstanding_amount * exc_rate_secondary_to_primary + + if self.doctype == "Sales Invoice": + # Calculate credit and debit values for reconciliation and advance entries + reconcilation_entry.credit_in_account_currency = self.outstanding_amount + reconcilation_entry.credit = os_in_default_currency + + advance_entry.debit_in_account_currency = os_in_primary_currency + advance_entry.debit = os_in_default_currency + else: + advance_entry.credit_in_account_currency = os_in_primary_currency + advance_entry.credit = os_in_default_currency + + reconcilation_entry.debit_in_account_currency = self.outstanding_amount + reconcilation_entry.debit = os_in_default_currency + + # Set exchange rates for entries + reconcilation_entry.exchange_rate = exc_rate_secondary_to_default + advance_entry.exchange_rate = exc_rate_primary_to_default else: - advance_entry.credit_in_account_currency = self.outstanding_amount - reconcilation_entry.debit_in_account_currency = self.outstanding_amount + if self.doctype == "Sales Invoice": + reconcilation_entry.credit_in_account_currency = self.outstanding_amount + advance_entry.debit_in_account_currency = self.outstanding_amount + else: + advance_entry.credit_in_account_currency = self.outstanding_amount + reconcilation_entry.debit_in_account_currency = self.outstanding_amount + jv.multi_currency = multi_currency jv.append("accounts", reconcilation_entry) jv.append("accounts", advance_entry) @@ -2506,6 +2613,67 @@ def repost_accounting_entries(self): repost_ledger.insert() repost_ledger.submit() + def get_advance_payment_doctypes(self) -> list: + return frappe.get_hooks("advance_payment_doctypes") + + def make_advance_payment_ledger_for_journal(self): + advance_payment_doctypes = self.get_advance_payment_doctypes() + advance_doctype_references = [ + x for x in self.accounts if x.reference_type in advance_payment_doctypes + ] + + for x in advance_doctype_references: + # Looking for payments + dr_or_cr = ( + "credit_in_account_currency" + if x.account_type == "Receivable" + else "debit_in_account_currency" + ) + + amount = x.get(dr_or_cr) + if amount > 0: + doc = frappe.new_doc("Advance Payment Ledger Entry") + doc.company = self.company + doc.voucher_type = self.doctype + doc.voucher_no = self.name + doc.against_voucher_type = x.reference_type + doc.against_voucher_no = x.reference_name + doc.amount = amount if self.docstatus == 1 else -1 * amount + doc.event = "Submit" if self.docstatus == 1 else "Cancel" + doc.currency = x.account_currency + doc.flags.ignore_permissions = 1 + doc.save() + + def make_advance_payment_ledger_for_payment(self): + advance_payment_doctypes = self.get_advance_payment_doctypes() + advance_doctype_references = [ + x for x in self.references if x.reference_doctype in advance_payment_doctypes + ] + currency = ( + self.paid_from_account_currency + if self.payment_type == "Receive" + else self.paid_to_account_currency + ) + for x in advance_doctype_references: + doc = frappe.new_doc("Advance Payment Ledger Entry") + doc.company = self.company + doc.voucher_type = self.doctype + doc.voucher_no = self.name + doc.against_voucher_type = x.reference_doctype + doc.against_voucher_no = x.reference_name + doc.amount = x.allocated_amount if self.docstatus == 1 else -1 * x.allocated_amount + doc.currency = currency + doc.event = "Submit" if self.docstatus == 1 else "Cancel" + doc.flags.ignore_permissions = 1 + doc.save() + + def make_advance_payment_ledger_entries(self): + if self.docstatus != 0: + if self.doctype == "Journal Entry": + self.make_advance_payment_ledger_for_journal() + elif self.doctype == "Payment Entry": + self.make_advance_payment_ledger_for_payment() + @frappe.whitelist() def get_tax_rate(account_head): @@ -2748,6 +2916,7 @@ def get_advance_payment_entries( party_account, order_doctype, order_list=None, + default_advance_account=None, include_unallocated=True, against_all_orders=False, limit=None, @@ -2761,6 +2930,7 @@ def get_advance_payment_entries( party_type, party, party_account, + default_advance_account, limit, condition, ) @@ -2784,6 +2954,7 @@ def get_advance_payment_entries( party_type, party, party_account, + default_advance_account, limit, condition, ) @@ -2799,6 +2970,7 @@ def get_common_query( party_type, party, party_account, + default_advance_account, limit, condition, ): @@ -2820,14 +2992,22 @@ def get_common_query( .where(payment_entry.docstatus == 1) ) - if payment_type == "Receive": - q = q.select((payment_entry.paid_from_account_currency).as_("currency")) - q = q.select(payment_entry.paid_from) - q = q.where(payment_entry.paid_from.isin(party_account)) + field = "paid_from" if payment_type == "Receive" else "paid_to" + + q = q.select((payment_entry[f"{field}_account_currency"]).as_("currency")) + q = q.select(payment_entry[field]) + account_condition = payment_entry[field].isin(party_account) + if default_advance_account: + q = q.where( + account_condition + | ( + (payment_entry[field] == default_advance_account) + & (payment_entry.book_advance_payments_in_separate_party_account == 1) + ) + ) + else: - q = q.select((payment_entry.paid_to_account_currency).as_("currency")) - q = q.select(payment_entry.paid_to) - q = q.where(payment_entry.paid_to.isin(party_account)) + q = q.where(account_condition) if payment_type == "Receive": q = q.select((payment_entry.source_exchange_rate).as_("exchange_rate")) @@ -3085,9 +3265,9 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True) if not child_item.warehouse: frappe.throw( - _("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.").format( - frappe.bold("default warehouse"), frappe.bold(item.item_code) - ) + _( + "Cannot find a default warehouse for item {0}. Please set one in the Item Master or in Stock Settings." + ).format(frappe.bold(item.item_code)) ) set_child_tax_template_and_map(item, child_item, p_doc) @@ -3285,7 +3465,6 @@ def validate_fg_item_for_subcontracting(new_data, is_new): items_added_or_removed = False # updated to true if any new item is added or removed any_conversion_factor_changed = False - sales_doctypes = ["Sales Order", "Sales Invoice", "Delivery Note", "Quotation"] parent = frappe.get_doc(parent_doctype, parent_doctype_name) check_doc_permissions(parent, "write") @@ -3401,25 +3580,21 @@ def validate_fg_item_for_subcontracting(new_data, is_new): # if rate is greater than price_list_rate, set margin # or set discount child_item.discount_percentage = 0 - - if parent_doctype in sales_doctypes: - child_item.margin_type = "Amount" - child_item.margin_rate_or_amount = flt( - child_item.rate - child_item.price_list_rate, - child_item.precision("margin_rate_or_amount"), - ) - child_item.rate_with_margin = child_item.rate + child_item.margin_type = "Amount" + child_item.margin_rate_or_amount = flt( + child_item.rate - child_item.price_list_rate, + child_item.precision("margin_rate_or_amount"), + ) + child_item.rate_with_margin = child_item.rate else: child_item.discount_percentage = flt( (1 - flt(child_item.rate) / flt(child_item.price_list_rate)) * 100.0, child_item.precision("discount_percentage"), ) child_item.discount_amount = flt(child_item.price_list_rate) - flt(child_item.rate) - - if parent_doctype in sales_doctypes: - child_item.margin_type = "" - child_item.margin_rate_or_amount = 0 - child_item.rate_with_margin = 0 + child_item.margin_type = "" + child_item.margin_rate_or_amount = 0 + child_item.rate_with_margin = 0 child_item.flags.ignore_validate_update_after_submit = True if new_child_flag: @@ -3494,6 +3669,9 @@ def validate_fg_item_for_subcontracting(new_data, is_new): parent.update_billing_percentage() parent.set_status() + parent.validate_uom_is_integer("uom", "qty") + parent.validate_uom_is_integer("stock_uom", "stock_qty") + # Cancel and Recreate Stock Reservation Entries. if parent_doctype == "Sales Order": from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index a55eded2a4cf..6020dce07615 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -356,14 +356,14 @@ def set_incoming_rate(self): if not self.is_internal_transfer(): return + self.set_sales_incoming_rate_for_internal_transfer() + allow_at_arms_length_price = frappe.get_cached_value( "Stock Settings", None, "allow_internal_transfer_at_arms_length_price" ) if allow_at_arms_length_price: return - self.set_sales_incoming_rate_for_internal_transfer() - for d in self.get("items"): d.discount_percentage = 0.0 d.discount_amount = 0.0 @@ -689,9 +689,11 @@ def on_submit(self): if self.doctype in ["Purchase Receipt", "Purchase Invoice"]: self.process_fixed_asset() - if self.doctype in ["Purchase Order", "Purchase Receipt"] and not frappe.db.get_single_value( - "Buying Settings", "disable_last_purchase_rate" - ): + if self.doctype in [ + "Purchase Order", + "Purchase Receipt", + "Purchase Invoice", + ] and not frappe.db.get_single_value("Buying Settings", "disable_last_purchase_rate"): update_last_purchase_rate(self, is_submit=1) def on_cancel(self): @@ -700,9 +702,11 @@ def on_cancel(self): if self.get("is_return"): return - if self.doctype in ["Purchase Order", "Purchase Receipt"] and not frappe.db.get_single_value( - "Buying Settings", "disable_last_purchase_rate" - ): + if self.doctype in [ + "Purchase Order", + "Purchase Receipt", + "Purchase Invoice", + ] and not frappe.db.get_single_value("Buying Settings", "disable_last_purchase_rate"): update_last_purchase_rate(self, is_submit=0) if self.doctype in ["Purchase Receipt", "Purchase Invoice"]: @@ -820,6 +824,8 @@ def make_asset(self, row, is_grouped_asset=False): "asset_quantity": asset_quantity, "purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None, "purchase_invoice": self.name if self.doctype == "Purchase Invoice" else None, + "purchase_receipt_item": row.name if self.doctype == "Purchase Receipt" else None, + "purchase_invoice_item": row.name if self.doctype == "Purchase Invoice" else None, } ) diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py index cc6870f892ae..5dace4af8842 100644 --- a/erpnext/controllers/item_variant.py +++ b/erpnext/controllers/item_variant.py @@ -150,7 +150,7 @@ def validate_item_attribute_value(attributes_list, attribute, attribute_value, i ) msg += "
" + _( "To still proceed with editing this Attribute Value, enable {0} in Item Variant Settings." - ).format(frappe.bold("Allow Rename Attribute Value")) + ).format(frappe.bold(_("Allow Rename Attribute Value"))) frappe.throw(msg, InvalidItemAttributeValueError, title=_("Edit Not Allowed")) diff --git a/erpnext/controllers/print_settings.py b/erpnext/controllers/print_settings.py index f99711631ffa..f05ef67c3087 100644 --- a/erpnext/controllers/print_settings.py +++ b/erpnext/controllers/print_settings.py @@ -11,7 +11,13 @@ def set_print_templates_for_item_table(doc, settings): "items": { "qty": "templates/print_formats/includes/item_table_qty.html", "serial_and_batch_bundle": "templates/print_formats/includes/serial_and_batch_bundle.html", - } + }, + "packed_items": { + "serial_and_batch_bundle": "templates/print_formats/includes/serial_and_batch_bundle.html", + }, + "supplied_items": { + "serial_and_batch_bundle": "templates/print_formats/includes/serial_and_batch_bundle.html", + }, } doc.flags.compact_item_fields = ["description", "qty", "rate", "amount"] diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index a07a00dd03ed..463cb8599702 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -366,7 +366,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): def get_empty_batches(filters, start, page_len, filtered_batches=None, txt=None): - query_filter = {"item": filters.get("item_code")} + query_filter = {"item": filters.get("item_code"), "disabled": 0} if txt: query_filter["name"] = ("like", f"%{txt}%") @@ -415,7 +415,6 @@ def get_batches_from_stock_ledger_entries(searchfields, txt, filters, start=0, p stock_ledger_entry.batch_no, Sum(stock_ledger_entry.actual_qty).as_("qty"), ) - .where((batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull())) .where(stock_ledger_entry.is_cancelled == 0) .where( (stock_ledger_entry.item_code == filters.get("item_code")) @@ -428,6 +427,9 @@ def get_batches_from_stock_ledger_entries(searchfields, txt, filters, start=0, p .limit(page_len) ) + if not filters.get("include_expired_batches"): + query = query.where((batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull())) + query = query.select( Concat("MFG-", batch_table.manufacturing_date).as_("manufacturing_date"), Concat("EXP-", batch_table.expiry_date).as_("expiry_date"), @@ -466,7 +468,6 @@ def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start=0 bundle.batch_no, Sum(bundle.qty).as_("qty"), ) - .where((batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull())) .where(stock_ledger_entry.is_cancelled == 0) .where( (stock_ledger_entry.item_code == filters.get("item_code")) @@ -479,6 +480,11 @@ def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start=0 .limit(page_len) ) + if not filters.get("include_expired_batches"): + bundle_query = bundle_query.where( + (batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull()) + ) + bundle_query = bundle_query.select( Concat("MFG-", batch_table.manufacturing_date), Concat("EXP-", batch_table.expiry_date), diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 3755506b6f53..696d404d16da 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -24,6 +24,10 @@ def validate_return(doc): if doc.return_against: validate_return_against(doc) + + if doc.doctype in ("Sales Invoice", "Purchase Invoice") and not doc.update_stock: + return + validate_returned_items(doc) @@ -79,6 +83,9 @@ def validate_returned_items(doc): if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]: select_fields += ",rejected_qty, received_qty" + if doc.doctype in ["Purchase Receipt", "Purchase Invoice"]: + select_fields += ",name" + for d in frappe.db.sql( f"""select {select_fields} from `tab{doc.doctype} Item` where parent = %s""", doc.return_against, @@ -104,15 +111,24 @@ def validate_returned_items(doc): items_returned = False for d in doc.get("items"): + key = d.item_code + raise_exception = False + if doc.doctype in ["Purchase Receipt", "Purchase Invoice"]: + field = frappe.scrub(doc.doctype) + "_item" + if d.get(field): + key = (d.item_code, d.get(field)) + raise_exception = True + if d.item_code and (flt(d.qty) < 0 or flt(d.get("received_qty")) < 0): - if d.item_code not in valid_items: - frappe.throw( + if key not in valid_items: + frappe.msgprint( _("Row # {0}: Returned Item {1} does not exist in {2} {3}").format( d.idx, d.item_code, doc.doctype, doc.return_against - ) + ), + raise_exception=raise_exception, ) else: - ref = valid_items.get(d.item_code, frappe._dict()) + ref = valid_items.get(key, frappe._dict()) validate_quantity(doc, d, ref, valid_items, already_returned_items) if ( @@ -193,8 +209,12 @@ def validate_quantity(doc, args, ref, valid_items, already_returned_items): def get_ref_item_dict(valid_items, ref_item_row): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + key = ref_item_row.item_code + if ref_item_row.get("name"): + key = (ref_item_row.item_code, ref_item_row.name) + valid_items.setdefault( - ref_item_row.item_code, + key, frappe._dict( { "qty": 0, @@ -208,7 +228,7 @@ def get_ref_item_dict(valid_items, ref_item_row): } ), ) - item_dict = valid_items[ref_item_row.item_code] + item_dict = valid_items[key] item_dict["qty"] += ref_item_row.qty item_dict["stock_qty"] += ref_item_row.get("stock_qty", 0) if ref_item_row.get("rate", 0) > item_dict["rate"]: @@ -335,6 +355,9 @@ def set_missing_values(source, target): doc.select_print_heading = frappe.get_cached_value("Print Heading", _("Debit Note")) if source.tax_withholding_category: doc.set_onload("supplier_tds", source.tax_withholding_category) + elif doctype == "Delivery Note": + # manual additions to the return should hit the return warehous, too + doc.set_warehouse = default_warehouse_for_sales_return for tax in doc.get("taxes") or []: if tax.charge_type == "Actual": @@ -581,6 +604,10 @@ def update_item(source_doc, target_doc, source_parent): if default_warehouse_for_sales_return: target_doc.warehouse = default_warehouse_for_sales_return + if not source_doc.use_serial_batch_fields and source_doc.serial_and_batch_bundle: + target_doc.serial_no = None + target_doc.batch_no = None + if ( (source_doc.serial_no or source_doc.batch_no) and not source_doc.serial_and_batch_bundle @@ -883,6 +910,7 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids): "`tabSerial and Batch Entry`.`serial_no`", "`tabSerial and Batch Entry`.`batch_no`", "`tabSerial and Batch Entry`.`qty`", + "`tabSerial and Batch Entry`.`incoming_rate`", "`tabSerial and Batch Bundle`.`voucher_detail_no`", "`tabSerial and Batch Bundle`.`voucher_type`", "`tabSerial and Batch Bundle`.`voucher_no`", @@ -904,15 +932,23 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids): if key not in available_dict: available_dict[key] = frappe._dict( - {"qty": 0.0, "serial_nos": defaultdict(float), "batches": defaultdict(float)} + { + "qty": 0.0, + "serial_nos": defaultdict(float), + "batches": defaultdict(float), + "serial_nos_valuation": defaultdict(float), + "batches_valuation": defaultdict(float), + } ) available_dict[key]["qty"] += row.qty if row.serial_no: available_dict[key]["serial_nos"][row.serial_no] += row.qty + available_dict[key]["serial_nos_valuation"][row.serial_no] = row.incoming_rate elif row.batch_no: available_dict[key]["batches"][row.batch_no] += row.qty + available_dict[key]["batches_valuation"][row.batch_no] = row.incoming_rate return available_dict @@ -948,12 +984,13 @@ def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False ) ) else: - fields = [ - "serial_and_batch_bundle", - ] + fields = ["serial_and_batch_bundle"] if is_rejected: - fields.extend(["rejected_serial_and_batch_bundle", "return_qty_from_rejected_warehouse"]) + fields.append("rejected_serial_and_batch_bundle") + + if doctype == "Purchase Receipt Item": + fields.append("return_qty_from_rejected_warehouse") del filters["rejected_serial_and_batch_bundle"] data = frappe.get_all( @@ -987,7 +1024,14 @@ def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field warehouse = row.get(warehouse_field) qty = abs(row.get(qty_field)) - filterd_serial_batch = frappe._dict({"serial_nos": [], "batches": defaultdict(float)}) + filterd_serial_batch = frappe._dict( + { + "serial_nos": [], + "batches": defaultdict(float), + "serial_nos_valuation": data.get("serial_nos_valuation"), + "batches_valuation": data.get("batches_valuation"), + } + ) if data.serial_nos: available_serial_nos = [] @@ -996,8 +1040,8 @@ def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field available_serial_nos.append(serial_no) if available_serial_nos: - if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]: - available_serial_nos = get_available_serial_nos(available_serial_nos) + if parent_doc.doctype in ["Purchase Invoice", "Purchase Receipt"]: + available_serial_nos = get_available_serial_nos(available_serial_nos, warehouse) if len(available_serial_nos) > qty: filterd_serial_batch["serial_nos"] = sorted(available_serial_nos[0 : cint(qty)]) @@ -1012,7 +1056,7 @@ def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field if batch_qty <= 0: continue - if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]: + if parent_doc.doctype in ["Purchase Invoice", "Purchase Receipt"]: batch_qty = get_available_batch_qty( parent_doc, batch_no, @@ -1082,6 +1126,8 @@ def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_f "warehouse": warehouse, "serial_nos": data.get("serial_nos"), "batches": data.get("batches"), + "serial_nos_valuation": data.get("serial_nos_valuation"), + "batches_valuation": data.get("batches_valuation"), "posting_date": parent_doc.posting_date, "posting_time": parent_doc.posting_time, "voucher_type": parent_doc.doctype, diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index c1f565e73239..b704cb30791d 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -21,9 +21,15 @@ def __setup__(self): def onload(self): super().onload() - if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"): + if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice", "Quotation"): for item in self.get("items") + (self.get("packed_items") or []): - item.update(get_bin_details(item.item_code, item.warehouse, include_child_warehouses=True)) + company = self.company + + item.update( + get_bin_details( + item.item_code, item.warehouse, company=company, include_child_warehouses=True + ) + ) def validate(self): super().validate() @@ -68,19 +74,13 @@ def set_missing_lead_customer_details(self, for_validate=False): if customer: from erpnext.accounts.party import _get_party_details - fetch_payment_terms_template = False - if self.get("__islocal") or self.company != frappe.db.get_value( - self.doctype, self.name, "company" - ): - fetch_payment_terms_template = True - party_details = _get_party_details( customer, ignore_permissions=self.flags.ignore_permissions, doctype=self.doctype, company=self.company, posting_date=self.get("posting_date"), - fetch_payment_terms_template=fetch_payment_terms_template, + fetch_payment_terms_template=self.has_value_changed("company"), party_address=self.customer_address, shipping_address=self.shipping_address_name, company_address=self.get("company_address"), @@ -167,6 +167,9 @@ def calculate_contribution(self): total = 0.0 sales_team = self.get("sales_team") + + self.validate_sales_team(sales_team) + for sales_person in sales_team: self.round_floats_in(sales_person) @@ -186,6 +189,20 @@ def calculate_contribution(self): if sales_team and total != 100.0: throw(_("Total allocated percentage for sales team should be 100")) + def validate_sales_team(self, sales_team): + sales_persons = [d.sales_person for d in sales_team] + + if not sales_persons: + return + + sales_person_status = frappe.db.get_all( + "Sales Person", filters={"name": ["in", sales_persons]}, fields=["name", "enabled"] + ) + + for row in sales_person_status: + if not row.enabled: + frappe.throw(_("Sales Person {0} is disabled.").format(row.name)) + def validate_max_discount(self): for d in self.get("items"): if d.item_code: @@ -358,12 +375,32 @@ def get_item_list(self): return il def has_product_bundle(self, item_code): - product_bundle = frappe.qb.DocType("Product Bundle") - return ( - frappe.qb.from_(product_bundle) - .select(product_bundle.name) - .where((product_bundle.new_item_code == item_code) & (product_bundle.disabled == 0)) - ).run() + product_bundle_items = getattr(self, "_product_bundle_items", None) + if product_bundle_items is None: + self._product_bundle_items = product_bundle_items = {} + + if item_code not in product_bundle_items: + self._fetch_product_bundle_items(item_code) + + return product_bundle_items[item_code] + + def _fetch_product_bundle_items(self, item_code): + product_bundle_items = self._product_bundle_items + items_to_fetch = {row.item_code for row in self.items if row.item_code not in product_bundle_items} + # fetch for requisite item_code even if it is not in items + items_to_fetch.add(item_code) + + items_with_product_bundle = { + row.new_item_code + for row in frappe.get_all( + "Product Bundle", + filters={"new_item_code": ("in", items_to_fetch), "disabled": 0}, + fields="new_item_code", + ) + } + + for item_code in items_to_fetch: + product_bundle_items[item_code] = item_code in items_with_product_bundle def get_already_delivered_qty(self, current_docname, so, so_detail): delivered_via_dn = frappe.db.sql( @@ -473,6 +510,16 @@ def set_incoming_rate(self): raise_error_if_no_rate=False, ) + if ( + not d.incoming_rate + and self.get("return_against") + and self.get("is_return") + and get_valuation_method(d.item_code) == "Moving Average" + ): + d.incoming_rate = get_rate_for_return( + self.doctype, self.name, d.item_code, self.return_against, item_row=d + ) + # For internal transfers use incoming rate as the valuation rate if self.is_internal_transfer(): if self.doctype == "Delivery Note" or self.get("update_stock"): @@ -640,7 +687,7 @@ def set_gross_profit(self): if self.doctype in ["Sales Order", "Quotation"]: for item in self.items: item.gross_profit = flt( - ((item.base_rate - flt(item.valuation_rate)) * item.stock_qty), + ((flt(item.stock_uom_rate) - flt(item.valuation_rate)) * item.stock_qty), self.precision("amount", item), ) @@ -697,7 +744,7 @@ def validate_for_duplicate_items(self): duplicate_items_msg = _("Item {0} entered multiple times.").format(frappe.bold(d.item_code)) duplicate_items_msg += "

" duplicate_items_msg += _("Please enable {} in {} to allow same item in multiple rows").format( - frappe.bold("Allow Item to Be Added Multiple Times in a Transaction"), + frappe.bold(_("Allow Item to Be Added Multiple Times in a Transaction")), get_link_to_form("Selling Settings", "Selling Settings"), ) if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1: diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 1620fa3bfffa..8f0a5d5edf52 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -63,6 +63,33 @@ def validate(self): self.set_rate_of_stock_uom() self.validate_internal_transfer() self.validate_putaway_capacity() + self.reset_conversion_factor() + + def reset_conversion_factor(self): + for row in self.get("items"): + if row.uom != row.stock_uom: + continue + + if row.conversion_factor != 1.0: + row.conversion_factor = 1.0 + frappe.msgprint( + _( + "Conversion factor for item {0} has been reset to 1.0 as the uom {1} is same as stock uom {2}." + ).format(bold(row.item_code), bold(row.uom), bold(row.stock_uom)), + alert=True, + ) + + def validate_items_exist(self): + if not self.get("items"): + return + + items = [d.item_code for d in self.get("items")] + + exists_items = frappe.get_all("Item", filters={"name": ("in", items)}, pluck="name") + non_exists_items = set(items) - set(exists_items) + + if non_exists_items: + frappe.throw(_("Items {0} do not exist in the Item master.").format(", ".join(non_exists_items))) def validate_duplicate_serial_and_batch_bundle(self, table_name): if not self.get(table_name): @@ -307,6 +334,11 @@ def make_bundle_for_non_rejected_qty(self, table_name): } ) + if self.doctype in ["Sales Invoice", "Delivery Note"]: + row.db_set( + "incoming_rate", frappe.db.get_value("Serial and Batch Bundle", bundle, "avg_rate") + ) + def get_reference_ids(self, table_name, qty_field=None, bundle_field=None) -> tuple[str, list[str]]: field = { "Sales Invoice": "sales_invoice_item", @@ -345,6 +377,9 @@ def get_reference_ids(self, table_name, qty_field=None, bundle_field=None) -> tu @frappe.request_cache def is_serial_batch_item(self, item_code) -> bool: + if not frappe.db.exists("Item", item_code): + frappe.throw(_("Item {0} does not exist.").format(bold(item_code))) + item_details = frappe.db.get_value("Item", item_code, ["has_serial_no", "has_batch_no"], as_dict=1) if item_details.has_serial_no or item_details.has_batch_no: @@ -804,6 +839,15 @@ def update_inventory_dimensions(self, row, sl_dict) -> None: if not dimension: continue + if ( + self.doctype in ["Purchase Invoice", "Purchase Receipt"] + and row.get("rejected_warehouse") + and sl_dict.get("warehouse") == row.get("rejected_warehouse") + ): + fieldname = f"rejected_{dimension.source_fieldname}" + sl_dict[dimension.target_fieldname] = row.get(fieldname) + continue + if self.doctype in [ "Purchase Invoice", "Purchase Receipt", @@ -964,11 +1008,13 @@ def validate_inspection(self): def validate_qi_presence(self, row): """Check if QI is present on row level. Warn on save and stop on submit if missing.""" if not row.quality_inspection: - msg = f"Row #{row.idx}: Quality Inspection is required for Item {frappe.bold(row.item_code)}" + msg = _("Row #{0}: Quality Inspection is required for Item {1}").format( + row.idx, frappe.bold(row.item_code) + ) if self.docstatus == 1: - frappe.throw(_(msg), title=_("Inspection Required"), exc=QualityInspectionRequiredError) + frappe.throw(msg, title=_("Inspection Required"), exc=QualityInspectionRequiredError) else: - frappe.msgprint(_(msg), title=_("Inspection Required"), indicator="blue") + frappe.msgprint(msg, title=_("Inspection Required"), indicator="blue") def validate_qi_submission(self, row): """Check if QI is submitted on row level, during submission""" @@ -977,11 +1023,13 @@ def validate_qi_submission(self, row): if not qa_docstatus == 1: link = frappe.utils.get_link_to_form("Quality Inspection", row.quality_inspection) - msg = f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}" + msg = _("Row #{0}: Quality Inspection {1} is not submitted for the item: {2}").format( + row.idx, link, row.item_code + ) if action == "Stop": - frappe.throw(_(msg), title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError) + frappe.throw(msg, title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError) else: - frappe.msgprint(_(msg), alert=True, indicator="orange") + frappe.msgprint(msg, alert=True, indicator="orange") def validate_qi_rejection(self, row): """Check if QI is rejected on row level, during submission""" @@ -990,11 +1038,13 @@ def validate_qi_rejection(self, row): if qa_status == "Rejected": link = frappe.utils.get_link_to_form("Quality Inspection", row.quality_inspection) - msg = f"Row #{row.idx}: Quality Inspection {link} was rejected for item {row.item_code}" + msg = _("Row #{0}: Quality Inspection {1} was rejected for item {2}").format( + row.idx, link, row.item_code + ) if action == "Stop": - frappe.throw(_(msg), title=_("Inspection Rejected"), exc=QualityInspectionRejectedError) + frappe.throw(msg, title=_("Inspection Rejected"), exc=QualityInspectionRejectedError) else: - frappe.msgprint(_(msg), alert=True, indicator="orange") + frappe.msgprint(msg, alert=True, indicator="orange") def update_blanket_order(self): blanket_orders = list(set([d.blanket_order for d in self.items if d.blanket_order])) diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index a6727ef8826f..5fb1ee468cda 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -561,11 +561,11 @@ def __add_supplied_item(self, item_row, bom_item, qty): use_serial_batch_fields = frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields") if self.doctype == self.subcontract_data.order_doctype: - rm_obj.required_qty = qty - rm_obj.amount = rm_obj.required_qty * rm_obj.rate + rm_obj.required_qty = flt(qty, rm_obj.precision("required_qty")) + rm_obj.amount = flt(rm_obj.required_qty * rm_obj.rate, rm_obj.precision("amount")) else: - rm_obj.consumed_qty = qty - rm_obj.required_qty = bom_item.required_qty or qty + rm_obj.consumed_qty = flt(qty, rm_obj.precision("consumed_qty")) + rm_obj.required_qty = flt(bom_item.required_qty or qty, rm_obj.precision("required_qty")) rm_obj.serial_and_batch_bundle = None setattr( rm_obj, self.subcontract_data.order_field, item_row.get(self.subcontract_data.order_field) @@ -576,30 +576,56 @@ def __add_supplied_item(self, item_row, bom_item, qty): self.__set_batch_nos(bom_item, item_row, rm_obj, qty) if self.doctype == "Subcontracting Receipt" and not use_serial_batch_fields: - args = frappe._dict( - { - "item_code": rm_obj.rm_item_code, - "warehouse": self.supplier_warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "qty": -1 * flt(rm_obj.consumed_qty), - "actual_qty": -1 * flt(rm_obj.consumed_qty), - "voucher_type": self.doctype, - "voucher_no": self.name, - "voucher_detail_no": item_row.name, - "company": self.company, - "allow_zero_valuation": 1, - } - ) - rm_obj.serial_and_batch_bundle = self.__set_serial_and_batch_bundle( item_row, rm_obj, rm_obj.consumed_qty ) - if rm_obj.serial_and_batch_bundle: - args["serial_and_batch_bundle"] = rm_obj.serial_and_batch_bundle + self.set_rate_for_supplied_items(rm_obj, item_row) + + def update_rate_for_supplied_items(self): + if self.doctype != "Subcontracting Receipt": + return + + for row in self.supplied_items: + item_row = None + if row.reference_name: + item_row = self.get_item_row(row.reference_name) + + if not item_row: + continue - rm_obj.rate = get_incoming_rate(args) + self.set_rate_for_supplied_items(row, item_row) + + def get_item_row(self, reference_name): + for item in self.items: + if item.name == reference_name: + return item + + def set_rate_for_supplied_items(self, rm_obj, item_row): + args = frappe._dict( + { + "item_code": rm_obj.rm_item_code, + "warehouse": self.supplier_warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "qty": -1 * flt(rm_obj.consumed_qty), + "actual_qty": -1 * flt(rm_obj.consumed_qty), + "voucher_type": self.doctype, + "voucher_no": self.name, + "voucher_detail_no": item_row.name, + "company": self.company, + "allow_zero_valuation": 1, + } + ) + + if rm_obj.serial_and_batch_bundle: + args["serial_and_batch_bundle"] = rm_obj.serial_and_batch_bundle + + if rm_obj.use_serial_batch_fields: + args["batch_no"] = rm_obj.batch_no + args["serial_no"] = rm_obj.serial_no + + rm_obj.rate = get_incoming_rate(args) def __set_batch_nos(self, bom_item, item_row, rm_obj, qty): key = (rm_obj.rm_item_code, item_row.item_code, item_row.get(self.subcontract_data.order_field)) @@ -638,8 +664,8 @@ def __set_batch_nos(self, bom_item, item_row, rm_obj, qty): self.__set_serial_nos(item_row, rm_obj) def __set_consumed_qty(self, rm_obj, consumed_qty, required_qty=0): - rm_obj.required_qty = required_qty - rm_obj.consumed_qty = consumed_qty + rm_obj.required_qty = flt(required_qty, rm_obj.precision("required_qty")) + rm_obj.consumed_qty = flt(consumed_qty, rm_obj.precision("consumed_qty")) def __set_serial_nos(self, item_row, rm_obj): key = (rm_obj.rm_item_code, item_row.item_code, item_row.get(self.subcontract_data.order_field)) @@ -1209,6 +1235,17 @@ def add_items_in_ste(ste_doc, row, qty, rm_details, rm_detail_field="sco_rm_deta def make_return_stock_entry_for_subcontract( available_materials, order_doc, rm_details, order_doctype="Subcontracting Order" ): + def post_process(source_doc, target_doc): + target_doc.purpose = "Material Transfer" + + if source_doc.doctype == "Purchase Order": + target_doc.purchase_order = source_doc.name + else: + target_doc.subcontracting_order = source_doc.name + + target_doc.company = source_doc.company + target_doc.is_return = 1 + ste_doc = get_mapped_doc( order_doctype, order_doc.name, @@ -1219,18 +1256,13 @@ def make_return_stock_entry_for_subcontract( }, }, ignore_child_tables=True, + postprocess=post_process, ) - ste_doc.purpose = "Material Transfer" - if order_doctype == "Purchase Order": - ste_doc.purchase_order = order_doc.name rm_detail_field = "po_detail" else: - ste_doc.subcontracting_order = order_doc.name rm_detail_field = "sco_rm_detail" - ste_doc.company = order_doc.company - ste_doc.is_return = 1 for _key, value in available_materials.items(): if not value.qty: diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 2d3b224b76f5..bf5beab1a82b 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -8,6 +8,7 @@ from frappe import _, scrub from frappe.model.document import Document from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction +from frappe.utils.deprecations import deprecated import erpnext from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_rate @@ -40,7 +41,7 @@ def filter_rows(self): return items def calculate(self): - if not len(self._items): + if not len(self.doc.items): return self.discount_amount_applied = False @@ -74,7 +75,7 @@ def _calculate(self): self.calculate_net_total() self.calculate_tax_withholding_net_total() self.calculate_taxes() - self.manipulate_grand_total_for_inclusive_tax() + self.adjust_grand_total_for_inclusive_tax() self.calculate_totals() self._cleanup() self.calculate_total_net_weight() @@ -95,7 +96,7 @@ def validate_item_tax_template(self): if self.doc.get("is_return") and self.doc.get("return_against"): return - for item in self._items: + for item in self.doc.items: if item.item_code and item.get("item_tax_template"): item_doc = frappe.get_cached_doc("Item", item.item_code) args = { @@ -154,7 +155,7 @@ def calculate_item_values(self): return if not self.discount_amount_applied: - for item in self._items: + for item in self.doc.items: self.doc.round_floats_in(item) if item.discount_percentage == 100: @@ -258,7 +259,7 @@ def determine_exclusive_rate(self): if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")): return - for item in self._items: + for item in self.doc.items: item_tax_map = self._load_item_tax_rate(item.item_tax_rate) cumulated_tax_fraction = 0 total_inclusive_tax_amount_per_qty = 0 @@ -286,7 +287,7 @@ def determine_exclusive_rate(self): ): amount = flt(item.amount) - total_inclusive_tax_amount_per_qty - item.net_amount = flt(amount / (1 + cumulated_tax_fraction)) + item.net_amount = flt(amount / (1 + cumulated_tax_fraction), item.precision("net_amount")) item.net_rate = flt(item.net_amount / item.qty, item.precision("net_rate")) item.discount_percentage = flt( item.discount_percentage, item.precision("discount_percentage") @@ -531,7 +532,12 @@ def round_off_base_values(self, tax): tax.base_tax_amount = round(tax.base_tax_amount, 0) tax.base_tax_amount_after_discount_amount = round(tax.base_tax_amount_after_discount_amount, 0) + @deprecated def manipulate_grand_total_for_inclusive_tax(self): + # for backward compatablility - if in case used by an external application + return self.adjust_grand_total_for_inclusive_tax() + + def adjust_grand_total_for_inclusive_tax(self): # if fully inclusive taxes and diff if self.doc.get("taxes") and any(cint(t.included_in_print_rate) for t in self.doc.get("taxes")): last_tax = self.doc.get("taxes")[-1] @@ -553,17 +559,21 @@ def manipulate_grand_total_for_inclusive_tax(self): diff = flt(diff, self.doc.precision("rounding_adjustment")) if diff and abs(diff) <= (5.0 / 10 ** last_tax.precision("tax_amount")): - self.doc.rounding_adjustment = diff + self.doc.grand_total_diff = diff + else: + self.doc.grand_total_diff = 0 def calculate_totals(self): if self.doc.get("taxes"): - self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + flt(self.doc.rounding_adjustment) + self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + flt( + self.doc.get("grand_total_diff") + ) else: self.doc.grand_total = flt(self.doc.net_total) if self.doc.get("taxes"): self.doc.total_taxes_and_charges = flt( - self.doc.grand_total - self.doc.net_total - flt(self.doc.rounding_adjustment), + self.doc.grand_total - self.doc.net_total - flt(self.doc.get("grand_total_diff")), self.doc.precision("total_taxes_and_charges"), ) else: @@ -626,8 +636,8 @@ def set_rounded_total(self): self.doc.grand_total, self.doc.currency, self.doc.precision("rounded_total") ) - # if print_in_rate is set, we would have already calculated rounding adjustment - self.doc.rounding_adjustment += flt( + # rounding adjustment should always be the difference vetween grand and rounded total + self.doc.rounding_adjustment = flt( self.doc.rounded_total - self.doc.grand_total, self.doc.precision("rounding_adjustment") ) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index b2f8fce3d312..289a955f9801 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -807,6 +807,7 @@ def test_15_gain_loss_on_different_posting_date(self): @change_settings("Stock Settings", {"allow_internal_transfer_at_arms_length_price": 1}) def test_16_internal_transfer_at_arms_length_price(self): + from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_purchase_invoice from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse prepare_data_for_internal_transfer() @@ -840,6 +841,31 @@ def test_16_internal_transfer_at_arms_length_price(self): # rate should reset to incoming rate self.assertEqual(si.items[0].rate, 100) + si.update_stock = 0 + si.save() + si.submit() + + pi = make_inter_company_purchase_invoice(si.name) + pi.update_stock = 1 + pi.items[0].rate = arms_length_price + pi.items[0].warehouse = target_warehouse + pi.items[0].from_warehouse = warehouse + pi.save() + + self.assertEqual(pi.items[0].rate, 100) + self.assertEqual(pi.items[0].valuation_rate, 100) + + frappe.db.set_single_value("Stock Settings", "allow_internal_transfer_at_arms_length_price", 1) + pi = make_inter_company_purchase_invoice(si.name) + pi.update_stock = 1 + pi.items[0].rate = arms_length_price + pi.items[0].warehouse = target_warehouse + pi.items[0].from_warehouse = warehouse + pi.save() + + self.assertEqual(pi.items[0].rate, arms_length_price) + self.assertEqual(pi.items[0].valuation_rate, 100) + def test_20_journal_against_sales_invoice(self): # Invoice in Foreign Currency si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1) diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py index dfd4351dcad5..84326bafef2e 100644 --- a/erpnext/controllers/tests/test_subcontracting_controller.py +++ b/erpnext/controllers/tests/test_subcontracting_controller.py @@ -1234,6 +1234,7 @@ def make_subcontracted_items(): "Subcontracted Item SA6": {}, "Subcontracted Item SA7": {}, "Subcontracted Item SA8": {}, + "Subcontracted Item SA9": {"stock_uom": "Litre"}, } for item, properties in sub_contracted_items.items(): @@ -1254,6 +1255,7 @@ def make_raw_materials(): "Subcontracted SRM Item 4": {"has_serial_no": 1, "serial_no_series": "SRII.####"}, "Subcontracted SRM Item 5": {"has_serial_no": 1, "serial_no_series": "SRIID.####"}, "Subcontracted SRM Item 8": {}, + "Subcontracted SRM Item 9": {"stock_uom": "Litre"}, } for item, properties in raw_materials.items(): @@ -1280,6 +1282,7 @@ def make_service_items(): "Subcontracted Service Item 6": {}, "Subcontracted Service Item 7": {}, "Subcontracted Service Item 8": {}, + "Subcontracted Service Item 9": {}, } for item, properties in service_items.items(): diff --git a/erpnext/controllers/trends.py b/erpnext/controllers/trends.py index 18fe7767c5d6..24d11e6050ac 100644 --- a/erpnext/controllers/trends.py +++ b/erpnext/controllers/trends.py @@ -69,13 +69,15 @@ def get_data(filters, conditions): "Delivery Note", ]: posting_date = "t1.posting_date" - if filters.period_based_on: + if filters.period_based_on and conditions.get("trans") in ["Sales Invoice", "Purchase Invoice"]: posting_date = "t1." + filters.period_based_on if conditions["based_on_select"] in ["t1.project,", "t2.project,"]: cond = " and " + conditions["based_on_select"][:-1] + " IS Not NULL" - if conditions.get("trans") in ["Sales Order", "Purchase Order"]: - cond += " and t1.status != 'Closed'" + + if not filters.get("include_closed_orders"): + if conditions.get("trans") in ["Sales Order", "Purchase Order"]: + cond += " and t1.status != 'Closed'" if conditions.get("trans") == "Quotation" and filters.get("group_by") == "Customer": cond += " and t1.quotation_to = 'Customer'" @@ -222,7 +224,7 @@ def period_wise_columns_query(filters, trans): if trans in ["Purchase Receipt", "Delivery Note", "Purchase Invoice", "Sales Invoice"]: trans_date = "posting_date" - if filters.period_based_on: + if filters.period_based_on and trans in ["Purchase Invoice", "Sales Invoice"]: trans_date = filters.period_based_on else: trans_date = "transaction_date" diff --git a/erpnext/crm/doctype/lead/lead.js b/erpnext/crm/doctype/lead/lead.js index 609eab7f9a27..325ee3d8d6ec 100644 --- a/erpnext/crm/doctype/lead/lead.js +++ b/erpnext/crm/doctype/lead/lead.js @@ -7,9 +7,9 @@ cur_frm.email_field = "email_id"; erpnext.LeadController = class LeadController extends frappe.ui.form.Controller { setup() { this.frm.make_methods = { - Customer: this.make_customer, - Quotation: this.make_quotation, - Opportunity: this.make_opportunity, + Customer: this.make_customer.bind(this), + Quotation: this.make_quotation.bind(this), + Opportunity: this.make_opportunity.bind(this), }; // For avoiding integration issues. @@ -28,18 +28,18 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller erpnext.toggle_naming_series(); if (!this.frm.is_new() && doc.__onload && !doc.__onload.is_customer) { - this.frm.add_custom_button(__("Customer"), this.make_customer, __("Create")); - this.frm.add_custom_button( - __("Opportunity"), - function () { - me.frm.trigger("make_opportunity"); - }, - __("Create") - ); - this.frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create")); + this.frm.add_custom_button(__("Customer"), this.make_customer.bind(this), __("Create")); + this.frm.add_custom_button(__("Opportunity"), this.make_opportunity.bind(this), __("Create")); + this.frm.add_custom_button(__("Quotation"), this.make_quotation.bind(this), __("Create")); if (!doc.__onload.linked_prospects.length) { - this.frm.add_custom_button(__("Prospect"), this.make_prospect, __("Create")); - this.frm.add_custom_button(__("Add to Prospect"), this.add_lead_to_prospect, __("Action")); + this.frm.add_custom_button(__("Prospect"), this.make_prospect.bind(this), __("Create")); + this.frm.add_custom_button( + __("Add to Prospect"), + () => { + this.add_lead_to_prospect(this.frm); + }, + __("Action") + ); } } @@ -53,8 +53,7 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller this.show_activities(); } - add_lead_to_prospect() { - let me = this; + add_lead_to_prospect(frm) { frappe.prompt( [ { @@ -69,12 +68,12 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller frappe.call({ method: "erpnext.crm.doctype.lead.lead.add_lead_to_prospect", args: { - lead: me.frm.doc.name, + lead: frm.doc.name, prospect: data.prospect, }, callback: function (r) { if (!r.exc) { - me.frm.reload_doc(); + frm.reload_doc(); } }, freeze: true, @@ -89,70 +88,19 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller make_customer() { frappe.model.open_mapped_doc({ method: "erpnext.crm.doctype.lead.lead.make_customer", - frm: cur_frm, + frm: this.frm, }); } make_quotation() { frappe.model.open_mapped_doc({ method: "erpnext.crm.doctype.lead.lead.make_quotation", - frm: cur_frm, - }); - } - - make_prospect() { - frappe.model.with_doctype("Prospect", function () { - let prospect = frappe.model.get_new_doc("Prospect"); - prospect.company_name = cur_frm.doc.company_name; - prospect.no_of_employees = cur_frm.doc.no_of_employees; - prospect.industry = cur_frm.doc.industry; - prospect.market_segment = cur_frm.doc.market_segment; - prospect.territory = cur_frm.doc.territory; - prospect.fax = cur_frm.doc.fax; - prospect.website = cur_frm.doc.website; - prospect.prospect_owner = cur_frm.doc.lead_owner; - prospect.notes = cur_frm.doc.notes; - - let leads_row = frappe.model.add_child(prospect, "leads"); - leads_row.lead = cur_frm.doc.name; - - frappe.set_route("Form", "Prospect", prospect.name); - }); - } - - company_name() { - if (!this.frm.doc.lead_name) { - this.frm.set_value("lead_name", this.frm.doc.company_name); - } - } - - show_notes() { - if (this.frm.doc.docstatus == 1) return; - - const crm_notes = new erpnext.utils.CRMNotes({ - frm: this.frm, - notes_wrapper: $(this.frm.fields_dict.notes_html.wrapper), - }); - crm_notes.refresh(); - } - - show_activities() { - if (this.frm.doc.docstatus == 1) return; - - const crm_activities = new erpnext.utils.CRMActivities({ frm: this.frm, - open_activities_wrapper: $(this.frm.fields_dict.open_activities_html.wrapper), - all_activities_wrapper: $(this.frm.fields_dict.all_activities_html.wrapper), - form_wrapper: $(this.frm.wrapper), }); - crm_activities.refresh(); } -}; -extend_cscript(cur_frm.cscript, new erpnext.LeadController({ frm: cur_frm })); - -frappe.ui.form.on("Lead", { - make_opportunity: async function (frm) { + async make_opportunity() { + const frm = this.frm; let existing_prospect = ( await frappe.db.get_value( "Prospect Lead", @@ -163,10 +111,11 @@ frappe.ui.form.on("Lead", { null, "Prospect" ) - ).message.name; + ).message?.name; + let fields = []; if (!existing_prospect) { - var fields = [ + fields.push( { label: "Create Prospect", fieldname: "create_prospect", @@ -179,9 +128,13 @@ frappe.ui.form.on("Lead", { fieldtype: "Data", default: frm.doc.company_name, depends_on: "create_prospect", - }, - ]; + mandatory_depends_on: "create_prospect", + } + ); } + + await frm.reload_doc(); + let existing_contact = ( await frappe.db.get_value( "Contact", @@ -191,7 +144,7 @@ frappe.ui.form.on("Lead", { }, "name" ) - ).message.name; + ).message?.name; if (!existing_contact) { fields.push({ @@ -202,12 +155,11 @@ frappe.ui.form.on("Lead", { }); } - if (fields) { - var d = new frappe.ui.Dialog({ + if (fields.length) { + const d = new frappe.ui.Dialog({ title: __("Create Opportunity"), fields: fields, - primary_action: function () { - var data = d.get_values(); + primary_action: function (data) { frappe.call({ method: "create_prospect_and_contact", doc: frm.doc, @@ -235,5 +187,56 @@ frappe.ui.form.on("Lead", { frm: frm, }); } - }, -}); + } + + make_prospect() { + const me = this; + frappe.model.with_doctype("Prospect", function () { + let prospect = frappe.model.get_new_doc("Prospect"); + prospect.company_name = me.frm.doc.company_name; + prospect.no_of_employees = me.frm.doc.no_of_employees; + prospect.industry = me.frm.doc.industry; + prospect.market_segment = me.frm.doc.market_segment; + prospect.territory = me.frm.doc.territory; + prospect.fax = me.frm.doc.fax; + prospect.website = me.frm.doc.website; + prospect.prospect_owner = me.frm.doc.lead_owner; + prospect.notes = me.frm.doc.notes; + + let leads_row = frappe.model.add_child(prospect, "leads"); + leads_row.lead = me.frm.doc.name; + + frappe.set_route("Form", "Prospect", prospect.name); + }); + } + + company_name() { + if (!this.frm.doc.lead_name) { + this.frm.set_value("lead_name", this.frm.doc.company_name); + } + } + + show_notes() { + if (this.frm.doc.docstatus == 1) return; + + const crm_notes = new erpnext.utils.CRMNotes({ + frm: this.frm, + notes_wrapper: $(this.frm.fields_dict.notes_html.wrapper), + }); + crm_notes.refresh(); + } + + show_activities() { + if (this.frm.doc.docstatus == 1) return; + + const crm_activities = new erpnext.utils.CRMActivities({ + frm: this.frm, + open_activities_wrapper: $(this.frm.fields_dict.open_activities_html.wrapper), + all_activities_wrapper: $(this.frm.fields_dict.all_activities_html.wrapper), + form_wrapper: $(this.frm.wrapper), + }); + crm_activities.refresh(); + } +}; + +extend_cscript(cur_frm.cscript, new erpnext.LeadController({ frm: cur_frm })); diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json index 07641d20c332..75373398b095 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.json +++ b/erpnext/crm/doctype/opportunity/opportunity.json @@ -185,7 +185,8 @@ { "fieldname": "expected_closing", "fieldtype": "Date", - "label": "Expected Closing Date" + "label": "Expected Closing Date", + "no_copy": 1 }, { "fieldname": "section_break_14", @@ -357,6 +358,7 @@ "fieldname": "transaction_date", "fieldtype": "Date", "label": "Opportunity Date", + "no_copy": 1, "oldfieldname": "transaction_date", "oldfieldtype": "Date", "reqd": 1, @@ -388,6 +390,7 @@ "fieldname": "first_response_time", "fieldtype": "Duration", "label": "First Response Time", + "no_copy": 1, "read_only": 1 }, { @@ -622,7 +625,7 @@ "icon": "fa fa-info-sign", "idx": 195, "links": [], - "modified": "2022-10-13 12:42:21.545636", + "modified": "2024-08-20 04:12:29.095761", "modified_by": "Administrator", "module": "CRM", "name": "Opportunity", diff --git a/erpnext/crm/doctype/prospect/test_prospect.py b/erpnext/crm/doctype/prospect/test_prospect.py index c3930ee6c93d..47e38515cac5 100644 --- a/erpnext/crm/doctype/prospect/test_prospect.py +++ b/erpnext/crm/doctype/prospect/test_prospect.py @@ -27,6 +27,29 @@ def test_add_lead_to_prospect_and_address_linking(self): address_doc.reload() self.assertEqual(address_doc.has_link("Prospect", prospect_doc.name), True) + def test_make_customer_from_prospect(self): + from erpnext.crm.doctype.prospect.prospect import make_customer as make_customer_from_prospect + + frappe.delete_doc_if_exists("Customer", "_Test Prospect") + + prospect = frappe.get_doc( + { + "doctype": "Prospect", + "company_name": "_Test Prospect", + "customer_group": "_Test Customer Group", + } + ) + prospect.insert() + + customer = make_customer_from_prospect("_Test Prospect") + + self.assertEqual(customer.doctype, "Customer") + self.assertEqual(customer.company_name, "_Test Prospect") + self.assertEqual(customer.customer_group, "_Test Customer Group") + + customer.company = "_Test Company" + customer.insert() + def make_prospect(**args): args = frappe._dict(args) diff --git a/erpnext/crm/frappe_crm_api.py b/erpnext/crm/frappe_crm_api.py new file mode 100644 index 000000000000..a00f0ba798db --- /dev/null +++ b/erpnext/crm/frappe_crm_api.py @@ -0,0 +1,173 @@ +import json + +import frappe +from frappe import _ +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields + + +@frappe.whitelist() +def create_custom_fields_for_frappe_crm(): + frappe.only_for("System Manager") + custom_fields = { + "Quotation": [ + { + "fieldname": "crm_deal", + "fieldtype": "Data", + "label": "Frappe CRM Deal", + "insert_after": "party_name", + } + ], + "Customer": [ + { + "fieldname": "crm_deal", + "fieldtype": "Data", + "label": "Frappe CRM Deal", + "insert_after": "prospect_name", + } + ], + } + create_custom_fields(custom_fields, ignore_validate=True) + + +@frappe.whitelist() +def create_prospect_against_crm_deal(): + frappe.only_for("System Manager") + doc = frappe.form_dict + prospect = frappe.get_doc( + { + "doctype": "Prospect", + "company_name": doc.organization or doc.lead_name, + "no_of_employees": doc.no_of_employees, + "prospect_owner": doc.deal_owner, + "company": doc.erpnext_company, + "crm_deal": doc.crm_deal, + "territory": doc.territory, + "industry": doc.industry, + "website": doc.website, + "annual_revenue": doc.annual_revenue, + } + ) + + try: + prospect_name = frappe.db.get_value("Prospect", {"company_name": prospect.company_name}) + if not prospect_name: + prospect.insert() + prospect_name = prospect.name + except Exception: + frappe.log_error( + frappe.get_traceback(), + f"Error while creating prospect against CRM Deal: {frappe.form_dict.get('crm_deal_id')}", + ) + pass + + create_contacts(json.loads(doc.contacts), prospect.company_name, "Prospect", prospect_name) + create_address("Prospect", prospect_name, doc.address) + frappe.response["message"] = prospect_name + + +def create_contacts(contacts, organization=None, link_doctype=None, link_docname=None): + for c in contacts: + c = frappe._dict(c) + existing_contact = contact_exists(c.email, c.mobile_no) + if existing_contact: + contact = frappe.get_doc("Contact", existing_contact) + else: + contact = frappe.get_doc( + { + "doctype": "Contact", + "first_name": c.get("full_name"), + "gender": c.get("gender"), + "company_name": organization, + } + ) + + if c.get("email"): + contact.append("email_ids", {"email_id": c.get("email"), "is_primary": 1}) + + if c.get("mobile_no"): + contact.append("phone_nos", {"phone": c.get("mobile_no"), "is_primary_mobile_no": 1}) + + link_doc(contact, link_doctype, link_docname) + + contact.save(ignore_permissions=True) + + +def create_address(doctype, docname, address): + if not address: + return + if isinstance(address, str): + address = json.loads(address) + try: + _address = frappe.db.exists("Address", address.get("name")) + if not _address: + new_address_doc = frappe.new_doc("Address") + for field in [ + "address_title", + "address_type", + "address_line1", + "address_line2", + "city", + "county", + "state", + "pincode", + "country", + ]: + if address.get(field): + new_address_doc.set(field, address.get(field)) + + new_address_doc.append("links", {"link_doctype": doctype, "link_name": docname}) + new_address_doc.insert(ignore_mandatory=True) + return new_address_doc.name + else: + address = frappe.get_doc("Address", _address) + link_doc(address, doctype, docname) + address.save(ignore_permissions=True) + return address.name + except Exception: + frappe.log_error(frappe.get_traceback(), f"Error while creating address for {docname}") + + +def link_doc(doc, link_doctype, link_docname): + already_linked = any( + [(link.link_doctype == link_doctype and link.link_name == link_docname) for link in doc.links] + ) + if not already_linked: + doc.append( + "links", {"link_doctype": link_doctype, "link_name": link_docname, "link_title": link_docname} + ) + + +def contact_exists(email, mobile_no): + email_exist = frappe.db.exists("Contact Email", {"email_id": email}) + mobile_exist = frappe.db.exists("Contact Phone", {"phone": mobile_no}) + + doctype = "Contact Email" if email_exist else "Contact Phone" + name = email_exist or mobile_exist + + if name: + return frappe.db.get_value(doctype, name, "parent") + + return False + + +@frappe.whitelist() +def create_customer(customer_data=None): + frappe.only_for("System Manager") + if not customer_data: + customer_data = frappe.form_dict + + try: + customer_name = frappe.db.exists("Customer", {"customer_name": customer_data.get("customer_name")}) + if not customer_name: + customer = frappe.get_doc({"doctype": "Customer", **customer_data}).insert( + ignore_permissions=True + ) + customer_name = customer.name + + contacts = json.loads(customer_data.get("contacts")) + create_contacts(contacts, customer_name, "Customer", customer_name) + create_address("Customer", customer_name, customer_data.get("address")) + return customer_name + except Exception: + frappe.log_error(frappe.get_traceback(), "Error while creating customer against Frappe CRM Deal") + pass diff --git a/erpnext/crm/number_card/open_opportunity/open_opportunity.json b/erpnext/crm/number_card/open_opportunity/open_opportunity.json index 6e06ed64da3d..33a757feb14d 100644 --- a/erpnext/crm/number_card/open_opportunity/open_opportunity.json +++ b/erpnext/crm/number_card/open_opportunity/open_opportunity.json @@ -3,7 +3,7 @@ "docstatus": 0, "doctype": "Number Card", "document_type": "Opportunity", - "dynamic_filters_json": "[[\"Opportunity\",\"status\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", + "dynamic_filters_json": "[[\"Opportunity\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", "filters_json": "[[\"Opportunity\",\"company\",\"=\",null,false]]", "function": "Count", "idx": 0, diff --git a/erpnext/edi/__init__.py b/erpnext/edi/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/edi/doctype/__init__.py b/erpnext/edi/doctype/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/edi/doctype/code_list/__init__.py b/erpnext/edi/doctype/code_list/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/edi/doctype/code_list/code_list.js b/erpnext/edi/doctype/code_list/code_list.js new file mode 100644 index 000000000000..f8b9a2003fd9 --- /dev/null +++ b/erpnext/edi/doctype/code_list/code_list.js @@ -0,0 +1,51 @@ +// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Code List", { + refresh: (frm) => { + if (!frm.doc.__islocal) { + frm.add_custom_button(__("Import Genericode File"), function () { + erpnext.edi.import_genericode(frm); + }); + } + }, + setup: (frm) => { + frm.savetrash = () => { + frm.validate_form_action("Delete"); + frappe.confirm( + __( + "Are you sure you want to delete {0}?

This action will also delete all associated Common Code documents.

", + [frm.docname.bold()] + ), + function () { + return frappe.call({ + method: "frappe.client.delete", + args: { + doctype: frm.doctype, + name: frm.docname, + }, + freeze: true, + freeze_message: __("Deleting {0} and all associated Common Code documents...", [ + frm.docname, + ]), + callback: function (r) { + if (!r.exc) { + frappe.utils.play_sound("delete"); + frappe.model.clear_doc(frm.doctype, frm.docname); + window.history.back(); + } + }, + }); + } + ); + }; + + frm.set_query("default_common_code", function (doc) { + return { + filters: { + code_list: doc.name, + }, + }; + }); + }, +}); diff --git a/erpnext/edi/doctype/code_list/code_list.json b/erpnext/edi/doctype/code_list/code_list.json new file mode 100644 index 000000000000..ffcc2f2b6055 --- /dev/null +++ b/erpnext/edi/doctype/code_list/code_list.json @@ -0,0 +1,112 @@ +{ + "actions": [], + "allow_copy": 1, + "allow_rename": 1, + "autoname": "prompt", + "creation": "2024-09-29 06:55:03.920375", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "title", + "canonical_uri", + "url", + "default_common_code", + "column_break_nkls", + "version", + "publisher", + "publisher_id", + "section_break_npxp", + "description" + ], + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title" + }, + { + "fieldname": "publisher", + "fieldtype": "Data", + "in_standard_filter": 1, + "label": "Publisher" + }, + { + "columns": 1, + "fieldname": "version", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Version" + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "label": "Description" + }, + { + "fieldname": "canonical_uri", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Canonical URI" + }, + { + "fieldname": "column_break_nkls", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_npxp", + "fieldtype": "Section Break" + }, + { + "fieldname": "publisher_id", + "fieldtype": "Data", + "in_standard_filter": 1, + "label": "Publisher ID" + }, + { + "fieldname": "url", + "fieldtype": "Data", + "label": "URL", + "options": "URL" + }, + { + "description": "This value shall be used when no matching Common Code for a record is found.", + "fieldname": "default_common_code", + "fieldtype": "Link", + "label": "Default Common Code", + "options": "Common Code" + } + ], + "index_web_pages_for_search": 1, + "links": [ + { + "link_doctype": "Common Code", + "link_fieldname": "code_list" + } + ], + "modified": "2024-11-16 17:01:40.260293", + "modified_by": "Administrator", + "module": "EDI", + "name": "Code List", + "naming_rule": "Set by user", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "search_fields": "description", + "show_title_field_in_link": 1, + "sort_field": "creation", + "sort_order": "DESC", + "states": [], + "title_field": "title" +} \ No newline at end of file diff --git a/erpnext/edi/doctype/code_list/code_list.py b/erpnext/edi/doctype/code_list/code_list.py new file mode 100644 index 000000000000..8957c6565b90 --- /dev/null +++ b/erpnext/edi/doctype/code_list/code_list.py @@ -0,0 +1,125 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from typing import TYPE_CHECKING + +import frappe +from frappe.model.document import Document + +if TYPE_CHECKING: + from lxml.etree import Element + + +class CodeList(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + canonical_uri: DF.Data | None + default_common_code: DF.Link | None + description: DF.SmallText | None + publisher: DF.Data | None + publisher_id: DF.Data | None + title: DF.Data | None + url: DF.Data | None + version: DF.Data | None + # end: auto-generated types + + def on_trash(self): + if not frappe.flags.in_bulk_delete: + self.__delete_linked_docs() + + def __delete_linked_docs(self): + self.db_set("default_common_code", None) + + linked_docs = frappe.get_all( + "Common Code", + filters={"code_list": self.name}, + fields=["name"], + ) + + for doc in linked_docs: + frappe.delete_doc("Common Code", doc.name) + + def get_codes_for(self, doctype: str, name: str) -> tuple[str]: + """Get the applicable codes for a doctype and name""" + return get_codes_for(self.name, doctype, name) + + def get_docnames_for(self, doctype: str, code: str) -> tuple[str]: + """Get the mapped docnames for a doctype and code""" + return get_docnames_for(self.name, doctype, code) + + def get_default_code(self) -> str | None: + """Get the default common code for this code list""" + return ( + frappe.db.get_value("Common Code", self.default_common_code, "common_code") + if self.default_common_code + else None + ) + + def from_genericode(self, root: "Element"): + """Extract Code List details from genericode XML""" + self.title = root.find(".//Identification/ShortName").text + self.version = root.find(".//Identification/Version").text + self.canonical_uri = root.find(".//CanonicalUri").text + # optionals + self.description = getattr(root.find(".//Identification/LongName"), "text", None) + self.publisher = getattr(root.find(".//Identification/Agency/ShortName"), "text", None) + if not self.publisher: + self.publisher = getattr(root.find(".//Identification/Agency/LongName"), "text", None) + self.publisher_id = getattr(root.find(".//Identification/Agency/Identifier"), "text", None) + self.url = getattr(root.find(".//Identification/LocationUri"), "text", None) + + +def get_codes_for(code_list: str, doctype: str, name: str) -> tuple[str]: + """Return the common code for a given record""" + CommonCode = frappe.qb.DocType("Common Code") + DynamicLink = frappe.qb.DocType("Dynamic Link") + + codes = ( + frappe.qb.from_(CommonCode) + .join(DynamicLink) + .on((CommonCode.name == DynamicLink.parent) & (DynamicLink.parenttype == "Common Code")) + .select(CommonCode.common_code) + .where( + (DynamicLink.link_doctype == doctype) + & (DynamicLink.link_name == name) + & (CommonCode.code_list == code_list) + ) + .distinct() + .orderby(CommonCode.common_code) + ).run() + + return tuple(c[0] for c in codes) if codes else () + + +def get_docnames_for(code_list: str, doctype: str, code: str) -> tuple[str]: + """Return the record name for a given common code""" + CommonCode = frappe.qb.DocType("Common Code") + DynamicLink = frappe.qb.DocType("Dynamic Link") + + docnames = ( + frappe.qb.from_(CommonCode) + .join(DynamicLink) + .on((CommonCode.name == DynamicLink.parent) & (DynamicLink.parenttype == "Common Code")) + .select(DynamicLink.link_name) + .where( + (DynamicLink.link_doctype == doctype) + & (CommonCode.common_code == code) + & (CommonCode.code_list == code_list) + ) + .distinct() + .orderby(DynamicLink.idx) + ).run() + + return tuple(d[0] for d in docnames) if docnames else () + + +def get_default_code(code_list: str) -> str | None: + """Return the default common code for a given code list""" + code_id = frappe.db.get_value("Code List", code_list, "default_common_code") + return frappe.db.get_value("Common Code", code_id, "common_code") if code_id else None diff --git a/erpnext/edi/doctype/code_list/code_list_import.js b/erpnext/edi/doctype/code_list/code_list_import.js new file mode 100644 index 000000000000..4a33f3e2fe6a --- /dev/null +++ b/erpnext/edi/doctype/code_list/code_list_import.js @@ -0,0 +1,218 @@ +frappe.provide("erpnext.edi"); + +erpnext.edi.import_genericode = function (listview_or_form) { + let doctype = "Code List"; + let docname = undefined; + if (listview_or_form.doc !== undefined) { + docname = listview_or_form.doc.name; + } + new frappe.ui.FileUploader({ + method: "erpnext.edi.doctype.code_list.code_list_import.import_genericode", + doctype: doctype, + docname: docname, + allow_toggle_private: false, + allow_take_photo: false, + on_success: function (_file_doc, r) { + listview_or_form.refresh(); + show_column_selection_dialog(r.message); + }, + }); +}; + +function show_column_selection_dialog(context) { + let title_description = __("If there is no title column, use the code column for the title."); + let default_title = get_default(context.columns, ["name", "Name", "code-name", "scheme-name"]); + let fields = [ + { + fieldtype: "HTML", + fieldname: "code_list_info", + options: `
${__( + "You are importing data for the code list:" + )} ${frappe.utils.get_form_link( + "Code List", + context.code_list, + true, + context.code_list_title + )}
`, + }, + { + fieldtype: "Section Break", + }, + { + fieldname: "import_column", + label: __("Import"), + fieldtype: "Column Break", + }, + { + fieldname: "title_column", + label: __("as Title"), + fieldtype: "Select", + reqd: 1, + options: context.columns, + default: default_title, + description: default_title ? null : title_description, + }, + { + fieldname: "code_column", + label: __("as Code"), + fieldtype: "Select", + options: context.columns, + reqd: 1, + default: get_default(context.columns, ["code", "Code", "value"]), + }, + { + fieldname: "filters_column", + label: __("Filter"), + fieldtype: "Column Break", + }, + ]; + + if (context.columns.length > 2) { + fields.splice(5, 0, { + fieldname: "description_column", + label: __("as Description"), + fieldtype: "Select", + options: [null].concat(context.columns), + default: get_default(context.columns, [ + "description", + "Description", + "remark", + __("description"), + __("Description"), + ]), + }); + } + + // Add filterable columns + for (let column in context.filterable_columns) { + fields.push({ + fieldname: `filter_${column}`, + label: __("by {}", [column]), + fieldtype: "Select", + options: [null].concat(context.filterable_columns[column]), + }); + } + + fields.push( + { + fieldname: "preview_section", + label: __("Preview"), + fieldtype: "Section Break", + }, + { + fieldname: "preview_html", + fieldtype: "HTML", + } + ); + + let d = new frappe.ui.Dialog({ + title: __("Select Columns and Filters"), + fields: fields, + primary_action_label: __("Import"), + size: "large", // This will make the modal wider + primary_action(values) { + let filters = {}; + for (let field in values) { + if (field.startsWith("filter_") && values[field]) { + filters[field.replace("filter_", "")] = values[field]; + } + } + frappe + .xcall("erpnext.edi.doctype.code_list.code_list_import.process_genericode_import", { + code_list_name: context.code_list, + file_name: context.file, + code_column: values.code_column, + title_column: values.title_column, + description_column: values.description_column, + filters: filters, + }) + .then((count) => { + frappe.msgprint(__("Import completed. {0} common codes created.", [count])); + }); + d.hide(); + }, + }); + + d.fields_dict.code_column.df.onchange = () => update_preview(d, context); + d.fields_dict.title_column.df.onchange = (e) => { + let field = d.fields_dict.title_column; + if (!e.target.value) { + field.df.description = title_description; + field.refresh(); + } else { + field.df.description = null; + field.refresh(); + } + update_preview(d, context); + }; + + // Add onchange events for filterable columns + for (let column in context.filterable_columns) { + d.fields_dict[`filter_${column}`].df.onchange = () => update_preview(d, context); + } + + d.show(); + update_preview(d, context); +} + +/** + * Return the first key from the keys array that is found in the columns array. + */ +function get_default(columns, keys) { + return keys.find((key) => columns.includes(key)); +} + +function update_preview(dialog, context) { + let code_column = dialog.get_value("code_column"); + let title_column = dialog.get_value("title_column"); + let description_column = dialog.get_value("description_column"); + + let html = ''; + if (title_column) html += ``; + if (code_column) html += ``; + if (description_column) html += ``; + + // Add headers for filterable columns + for (let column in context.filterable_columns) { + if (dialog.get_value(`filter_${column}`)) { + html += ``; + } + } + + html += ""; + + for (let i = 0; i < 3; i++) { + html += ""; + if (title_column) { + let title = context.example_values[title_column][i] || ""; + html += ``; + } + if (code_column) { + let code = context.example_values[code_column][i] || ""; + html += ``; + } + if (description_column) { + let description = context.example_values[description_column][i] || ""; + html += ``; + } + + // Add values for filterable columns + for (let column in context.filterable_columns) { + if (dialog.get_value(`filter_${column}`)) { + let value = context.example_values[column][i] || ""; + html += ``; + } + } + + html += ""; + } + + html += "
${__("Title")}${__("Code")}${__("Description")}${__(column)}
${truncate(title)}${truncate(code)}${truncate(description)}${truncate(value)}
"; + + dialog.fields_dict.preview_html.$wrapper.html(html); +} + +function truncate(value, maxLength = 40) { + if (typeof value !== "string") return ""; + return value.length > maxLength ? value.substring(0, maxLength - 3) + "..." : value; +} diff --git a/erpnext/edi/doctype/code_list/code_list_import.py b/erpnext/edi/doctype/code_list/code_list_import.py new file mode 100644 index 000000000000..50df3be471e6 --- /dev/null +++ b/erpnext/edi/doctype/code_list/code_list_import.py @@ -0,0 +1,140 @@ +import json + +import frappe +import requests +from frappe import _ +from lxml import etree + +URL_PREFIXES = ("http://", "https://") + + +@frappe.whitelist() +def import_genericode(): + doctype = "Code List" + docname = frappe.form_dict.docname + content = frappe.local.uploaded_file + + # recover the content, if it's a link + if (file_url := frappe.local.uploaded_file_url) and file_url.startswith(URL_PREFIXES): + try: + # If it's a URL, fetch the content and make it a local file (for durable audit) + response = requests.get(frappe.local.uploaded_file_url) + response.raise_for_status() + frappe.local.uploaded_file = content = response.content + frappe.local.uploaded_filename = frappe.local.uploaded_file_url.split("/")[-1] + frappe.local.uploaded_file_url = None + except Exception as e: + frappe.throw(f"
{e!s}
", title=_("Fetching Error")) + + if file_url := frappe.local.uploaded_file_url: + file_path = frappe.utils.file_manager.get_file_path(file_url) + with open(file_path.encode(), mode="rb") as f: + content = f.read() + + # Parse the xml content + parser = etree.XMLParser(remove_blank_text=True) + try: + root = etree.fromstring(content, parser=parser) + except Exception as e: + frappe.throw(f"
{e!s}
", title=_("Parsing Error")) + + # Extract the name (CanonicalVersionUri) from the parsed XML + name = root.find(".//CanonicalVersionUri").text + docname = docname or name + + if frappe.db.exists(doctype, docname): + code_list = frappe.get_doc(doctype, docname) + if code_list.name != name: + frappe.throw(_("The uploaded file does not match the selected Code List.")) + else: + # Create a new Code List document with the extracted name + code_list = frappe.new_doc(doctype) + code_list.name = name + + code_list.from_genericode(root) + code_list.save() + + # Attach the file and provide a recoverable identifier + file_doc = frappe.get_doc( + { + "doctype": "File", + "attached_to_doctype": "Code List", + "attached_to_name": code_list.name, + "folder": "Home/Attachments", + "file_name": frappe.local.uploaded_filename, + "file_url": frappe.local.uploaded_file_url, + "is_private": 1, + "content": content, + } + ).save() + + # Get available columns and example values + columns, example_values, filterable_columns = get_genericode_columns_and_examples(root) + + return { + "code_list": code_list.name, + "code_list_title": code_list.title, + "file": file_doc.name, + "columns": columns, + "example_values": example_values, + "filterable_columns": filterable_columns, + } + + +@frappe.whitelist() +def process_genericode_import( + code_list_name: str, + file_name: str, + code_column: str, + title_column: str | None = None, + description_column: str | None = None, + filters: str | None = None, +): + from erpnext.edi.doctype.common_code.common_code import import_genericode + + column_map = {"code": code_column, "title": title_column, "description": description_column} + + return import_genericode(code_list_name, file_name, column_map, json.loads(filters) if filters else None) + + +def get_genericode_columns_and_examples(root): + columns = [] + example_values = {} + filterable_columns = {} + + # Get column names + for column in root.findall(".//Column"): + column_id = column.get("Id") + columns.append(column_id) + example_values[column_id] = [] + filterable_columns[column_id] = set() + + # Get all values and count unique occurrences + for row in root.findall(".//SimpleCodeList/Row"): + for value in row.findall("Value"): + column_id = value.get("ColumnRef") + if column_id not in columns: + # Handle undeclared column + columns.append(column_id) + example_values[column_id] = [] + filterable_columns[column_id] = set() + + simple_value = value.find("./SimpleValue") + if simple_value is None: + continue + + filterable_columns[column_id].add(simple_value.text) + + # Get example values (up to 3) and filter columns with cardinality <= 5 + for row in root.findall(".//SimpleCodeList/Row")[:3]: + for value in row.findall("Value"): + column_id = value.get("ColumnRef") + simple_value = value.find("./SimpleValue") + if simple_value is None: + continue + + example_values[column_id].append(simple_value.text) + + filterable_columns = {k: list(v) for k, v in filterable_columns.items() if len(v) <= 5} + + return columns, example_values, filterable_columns diff --git a/erpnext/edi/doctype/code_list/code_list_list.js b/erpnext/edi/doctype/code_list/code_list_list.js new file mode 100644 index 000000000000..08125de2903f --- /dev/null +++ b/erpnext/edi/doctype/code_list/code_list_list.js @@ -0,0 +1,8 @@ +frappe.listview_settings["Code List"] = { + onload: function (listview) { + listview.page.add_inner_button(__("Import Genericode File"), function () { + erpnext.edi.import_genericode(listview); + }); + }, + hide_name_column: true, +}; diff --git a/erpnext/edi/doctype/code_list/test_code_list.py b/erpnext/edi/doctype/code_list/test_code_list.py new file mode 100644 index 000000000000..d37b1ee8f5a4 --- /dev/null +++ b/erpnext/edi/doctype/code_list/test_code_list.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestCodeList(FrappeTestCase): + pass diff --git a/erpnext/edi/doctype/common_code/__init__.py b/erpnext/edi/doctype/common_code/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/edi/doctype/common_code/common_code.js b/erpnext/edi/doctype/common_code/common_code.js new file mode 100644 index 000000000000..646d5c85b74f --- /dev/null +++ b/erpnext/edi/doctype/common_code/common_code.js @@ -0,0 +1,8 @@ +// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Common Code", { +// refresh(frm) { + +// }, +// }); diff --git a/erpnext/edi/doctype/common_code/common_code.json b/erpnext/edi/doctype/common_code/common_code.json new file mode 100644 index 000000000000..b2cb43fa5758 --- /dev/null +++ b/erpnext/edi/doctype/common_code/common_code.json @@ -0,0 +1,103 @@ +{ + "actions": [], + "autoname": "hash", + "creation": "2024-09-29 07:01:18.133067", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "code_list", + "title", + "common_code", + "description", + "column_break_wxsw", + "additional_data", + "section_break_rhgh", + "applies_to" + ], + "fields": [ + { + "fieldname": "code_list", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Code List", + "options": "Code List", + "reqd": 1, + "search_index": 1 + }, + { + "fieldname": "title", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Title", + "length": 300, + "reqd": 1 + }, + { + "fieldname": "column_break_wxsw", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_rhgh", + "fieldtype": "Section Break" + }, + { + "fieldname": "applies_to", + "fieldtype": "Table", + "label": "Applies To", + "options": "Dynamic Link" + }, + { + "fieldname": "common_code", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Common Code", + "length": 300, + "reqd": 1, + "search_index": 1 + }, + { + "fieldname": "additional_data", + "fieldtype": "Code", + "label": "Additional Data", + "max_height": "190px", + "read_only": 1 + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Description", + "max_height": "60px" + } + ], + "links": [], + "modified": "2024-11-06 07:46:17.175687", + "modified_by": "Administrator", + "module": "EDI", + "name": "Common Code", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "search_fields": "common_code,description", + "show_title_field_in_link": 1, + "sort_field": "creation", + "sort_order": "DESC", + "states": [], + "title_field": "title" +} \ No newline at end of file diff --git a/erpnext/edi/doctype/common_code/common_code.py b/erpnext/edi/doctype/common_code/common_code.py new file mode 100644 index 000000000000..d558b2d282f4 --- /dev/null +++ b/erpnext/edi/doctype/common_code/common_code.py @@ -0,0 +1,114 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import hashlib + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils.data import get_link_to_form +from lxml import etree + + +class CommonCode(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.core.doctype.dynamic_link.dynamic_link import DynamicLink + from frappe.types import DF + + additional_data: DF.Code | None + applies_to: DF.Table[DynamicLink] + code_list: DF.Link + common_code: DF.Data + description: DF.SmallText | None + title: DF.Data + # end: auto-generated types + + def validate(self): + self.validate_distinct_references() + + def validate_distinct_references(self): + """Ensure no two Common Codes of the same Code List are linked to the same document.""" + for link in self.applies_to: + existing_links = frappe.get_all( + "Common Code", + filters=[ + ["name", "!=", self.name], + ["code_list", "=", self.code_list], + ["Dynamic Link", "link_doctype", "=", link.link_doctype], + ["Dynamic Link", "link_name", "=", link.link_name], + ], + fields=["name", "common_code"], + ) + + if existing_links: + existing_link = existing_links[0] + frappe.throw( + _("{0} {1} is already linked to Common Code {2}.").format( + link.link_doctype, + link.link_name, + get_link_to_form("Common Code", existing_link["name"], existing_link["common_code"]), + ) + ) + + def from_genericode(self, column_map: dict, xml_element: "etree.Element"): + """Populate the Common Code document from a genericode XML element + + Args: + column_map (dict): A mapping of column names to XML column references. Keys: code, title, description + code (etree.Element): The XML element representing a code in the genericode file + """ + title_column = column_map.get("title") + code_column = column_map["code"] + description_column = column_map.get("description") + + self.common_code = xml_element.find(f"./Value[@ColumnRef='{code_column}']/SimpleValue").text + + if title_column: + simple_value_title = xml_element.find(f"./Value[@ColumnRef='{title_column}']/SimpleValue") + self.title = simple_value_title.text if simple_value_title is not None else self.common_code + + if description_column: + simple_value_descr = xml_element.find(f"./Value[@ColumnRef='{description_column}']/SimpleValue") + self.description = simple_value_descr.text if simple_value_descr is not None else None + + self.additional_data = etree.tostring(xml_element, encoding="unicode", pretty_print=True) + + +def simple_hash(input_string, length=6): + return hashlib.blake2b(input_string.encode(), digest_size=length // 2).hexdigest() + + +def import_genericode(code_list: str, file_name: str, column_map: dict, filters: dict | None = None): + """Import genericode file and create Common Code entries""" + file_path = frappe.utils.file_manager.get_file_path(file_name) + parser = etree.XMLParser(remove_blank_text=True) + tree = etree.parse(file_path, parser=parser) + root = tree.getroot() + + # Construct the XPath expression + xpath_expr = ".//SimpleCodeList/Row" + filter_conditions = [ + f"Value[@ColumnRef='{column_ref}']/SimpleValue='{value}'" for column_ref, value in filters.items() + ] + if filter_conditions: + xpath_expr += "[" + " and ".join(filter_conditions) + "]" + + elements = root.xpath(xpath_expr) + total_elements = len(elements) + for i, xml_element in enumerate(elements, start=1): + common_code: "CommonCode" = frappe.new_doc("Common Code") + common_code.code_list = code_list + common_code.from_genericode(column_map, xml_element) + common_code.save() + frappe.publish_progress(i / total_elements * 100, title=_("Importing Common Codes")) + + return total_elements + + +def on_doctype_update(): + frappe.db.add_index("Common Code", ["code_list", "common_code"]) diff --git a/erpnext/edi/doctype/common_code/common_code_list.js b/erpnext/edi/doctype/common_code/common_code_list.js new file mode 100644 index 000000000000..de1b665b1617 --- /dev/null +++ b/erpnext/edi/doctype/common_code/common_code_list.js @@ -0,0 +1,8 @@ +frappe.listview_settings["Common Code"] = { + onload: function (listview) { + listview.page.add_inner_button(__("Import Genericode File"), function () { + erpnext.edi.import_genericode(listview); + }); + }, + hide_name_column: true, +}; diff --git a/erpnext/edi/doctype/common_code/test_common_code.py b/erpnext/edi/doctype/common_code/test_common_code.py new file mode 100644 index 000000000000..e9c67b2cc829 --- /dev/null +++ b/erpnext/edi/doctype/common_code/test_common_code.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestCommonCode(FrappeTestCase): + pass diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index edfab4777a78..e187c0750776 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -96,7 +96,7 @@ def add_bank_accounts(response, bank, company): frappe.throw( _( "Please setup and enable a group account with the Account Type - {0} for the company {1}" - ).format(frappe.bold("Bank"), company) + ).format(frappe.bold(_("Bank")), company) ) for account in response["accounts"]: diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 527be6ab3378..882adec4d513 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -10,7 +10,17 @@ app_logo_url = "/assets/erpnext/images/erpnext-logo.svg" -develop_version = "14.x.x-develop" +add_to_apps_screen = [ + { + "name": "erpnext", + "logo": "/assets/erpnext/images/erpnext-logo-blue.png", + "title": "ERPNext", + "route": "/app/home", + "has_permission": "erpnext.check_app_permission", + } +] + +develop_version = "15.x.x-develop" app_include_js = "erpnext.bundle.js" app_include_css = "erpnext.bundle.css" @@ -25,6 +35,14 @@ "Newsletter": "public/js/newsletter.js", "Contact": "public/js/contact.js", } +doctype_list_js = { + "Code List": [ + "edi/doctype/code_list/code_list_import.js", + ], + "Common Code": [ + "edi/doctype/code_list/code_list_import.js", + ], +} override_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"} @@ -355,7 +373,6 @@ "Payment Entry": { "on_submit": [ "erpnext.regional.create_transaction_log", - "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status", "erpnext.accounts.doctype.dunning.dunning.resolve_dunning", ], "on_cancel": ["erpnext.accounts.doctype.dunning.dunning.resolve_dunning"], diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 6267ee4d029d..fe498ca32f96 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -182,25 +182,30 @@ frappe.ui.form.on("BOM", { }, make_work_order(frm) { - frm.events.setup_variant_prompt(frm, "Work Order", (frm, item, data, variant_items) => { - frappe.call({ - method: "erpnext.manufacturing.doctype.work_order.work_order.make_work_order", - args: { - bom_no: frm.doc.name, - item: item, - qty: data.qty || 0.0, - project: frm.doc.project, - variant_items: variant_items, - }, - freeze: true, - callback(r) { - if (r.message) { - let doc = frappe.model.sync(r.message)[0]; - frappe.set_route("Form", doc.doctype, doc.name); - } - }, - }); - }); + frm.events.setup_variant_prompt( + frm, + "Work Order", + (frm, item, data, variant_items, use_multi_level_bom) => { + frappe.call({ + method: "erpnext.manufacturing.doctype.work_order.work_order.make_work_order", + args: { + bom_no: frm.doc.name, + item: item, + qty: data.qty || 0.0, + project: frm.doc.project, + variant_items: variant_items, + use_multi_level_bom: use_multi_level_bom, + }, + freeze: true, + callback(r) { + if (r.message) { + let doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); + } + }, + }); + } + ); }, make_variant_bom(frm) { @@ -250,6 +255,15 @@ frappe.ui.form.on("BOM", { }); } + if (!skip_qty_field) { + fields.push({ + fieldtype: "Check", + label: __("Use Multi-Level BOM"), + fieldname: "use_multi_level_bom", + default: 1, + }); + } + if (!skip_qty_field) { fields.push({ fieldtype: "Float", @@ -277,6 +291,13 @@ frappe.ui.form.on("BOM", { cur_dialog.refresh(); }, }); + + fields.push({ + fieldtype: "Check", + label: __("Use Multi-Level BOM"), + fieldname: "use_multi_level_bom", + default: frm.doc?.__onload.use_multi_level_bom, + }); } var has_template_rm = frm.doc.items.filter((d) => d.has_variants === 1) || []; @@ -285,6 +306,7 @@ frappe.ui.form.on("BOM", { fieldname: "items", fieldtype: "Table", label: __("Raw Materials"), + depends_on: "eval:!doc.use_multi_level_bom", fields: [ { fieldname: "item_code", @@ -293,6 +315,13 @@ frappe.ui.form.on("BOM", { fieldtype: "Link", in_list_view: 1, reqd: 1, + get_query() { + return { + filters: { + has_variants: 1, + }, + }; + }, }, { fieldname: "variant_item_code", @@ -313,6 +342,13 @@ frappe.ui.form.on("BOM", { }, }; }, + change() { + let doc = this.doc; + if (!doc.qty) { + doc.qty = 1.0; + this.grid.set_value("qty", 1.0, doc); + } + }, }, { fieldname: "qty", @@ -347,14 +383,15 @@ frappe.ui.form.on("BOM", { (data) => { let item = data.item || frm.doc.item; let variant_items = data.items || []; + let use_multi_level_bom = data.use_multi_level_bom || 0; variant_items.forEach((d) => { - if (!d.variant_item_code) { + if (!d.variant_item_code && !use_multi_level_bom) { frappe.throw(__("Select variant item code for the template item {0}", [d.item_code])); } }); - callback(frm, item, data, variant_items); + callback(frm, item, data, variant_items, use_multi_level_bom); }, __(title), __("Create") @@ -364,7 +401,7 @@ frappe.ui.form.on("BOM", { dialog.fields_dict.items.df.data.push({ item_code: d.item_code, variant_item_code: "", - qty: d.qty, + qty: (d.qty / frm.doc.quantity) * (dialog.fields_dict.qty.value || 1), source_warehouse: d.source_warehouse, operation: d.operation, }); diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 303670cee726..6b2dd77c471b 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -176,13 +176,10 @@ def autoname(self): search_key = f"{self.doctype}-{self.item}%" existing_boms = frappe.get_all( - "BOM", filters={"name": ("like", search_key), "amended_from": ["is", "not set"]}, pluck="name" + "BOM", filters={"name": search_key, "amended_from": ["is", "not set"]}, pluck="name" ) - if existing_boms: - index = self.get_next_version_index(existing_boms) - else: - index = 1 + index = self.get_index_for_bom(existing_boms) prefix = self.doctype suffix = "%.3i" % index # convert index to string (1 -> "001") @@ -200,21 +197,40 @@ def autoname(self): name = f"{prefix}-{truncated_item_name}-{suffix}" if frappe.db.exists("BOM", name): - conflicting_bom = frappe.get_doc("BOM", name) - - if conflicting_bom.item != self.item: - msg = _("A BOM with name {0} already exists for item {1}.").format( - frappe.bold(name), frappe.bold(conflicting_bom.item) - ) + existing_boms = frappe.get_all( + "BOM", filters={"name": ("like", search_key), "amended_from": ["is", "not set"]}, pluck="name" + ) - frappe.throw( - _("{0}{1} Did you rename the item? Please contact Administrator / Tech support").format( - msg, "
" - ) - ) + index = self.get_index_for_bom(existing_boms) + suffix = "%.3i" % index + name = f"{prefix}-{self.item}-{suffix}" self.name = name + def get_index_for_bom(self, existing_boms): + index = 1 + if existing_boms: + index = self.get_next_version_index(existing_boms) + + return index + + def onload(self): + super().onload() + + self.set_onload_for_muulti_level_bom() + + def set_onload_for_muulti_level_bom(self): + use_multi_level_bom = frappe.db.get_value( + "Property Setter", + {"field_name": "use_multi_level_bom", "doc_type": "Work Order", "property": "default"}, + "value", + ) + + if use_multi_level_bom is None: + use_multi_level_bom = 1 + + self.set_onload("use_multi_level_bom", cint(use_multi_level_bom)) + @staticmethod def get_next_version_index(existing_boms: list[str]) -> int: # split by "/" and "-" @@ -260,6 +276,24 @@ def validate(self): self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False) self.set_process_loss_qty() self.validate_scrap_items() + self.set_default_uom() + + def set_default_uom(self): + if not self.get("items"): + return + + item_wise_uom = frappe._dict( + frappe.get_all( + "Item", + filters={"name": ("in", [item.item_code for item in self.items])}, + fields=["name", "stock_uom"], + as_list=1, + ) + ) + + for row in self.get("items"): + if row.stock_uom != item_wise_uom.get(row.item_code): + row.stock_uom = item_wise_uom.get(row.item_code) def get_context(self, context): context.parents = [{"name": "boms", "title": _("All BOMs")}] @@ -742,11 +776,8 @@ def calculate_rm_cost(self, save=False): base_total_rm_cost = 0 for d in self.get("items"): - if not d.is_stock_item and self.rm_cost_as_per == "Valuation Rate": - continue - old_rate = d.rate - if not self.bom_creator: + if not self.bom_creator and d.is_stock_item: d.rate = self.get_rm_rate( { "company": self.company, @@ -968,6 +999,13 @@ def validate_operations(self): if not d.batch_size or d.batch_size <= 0: d.batch_size = 1 + if not d.workstation and not d.workstation_type: + frappe.throw( + _( + "Row {0}: Workstation or Workstation Type is mandatory for an operation {1}" + ).format(d.idx, d.operation) + ) + def get_tree_representation(self) -> BOMTree: """Get a complete tree representation preserving order of child items.""" return BOMTree(self.name) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index e6ee59afb273..83e722cce50c 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -768,6 +768,26 @@ def test_get_scrap_items_from_sub_assemblies(self): for item_code in scraped_items.keys(): self.assertIn(item_code, bom_scraped_items, f"Item {item_code} not found in BOM scrap items") + def test_bom_raw_materials_stock_uom(self): + rm_item = make_item( + properties={"is_stock_item": 1, "valuation_rate": 1000.0, "stock_uom": "Nos"} + ).name + fg_item = make_item(properties={"is_stock_item": 1}).name + + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + + bom = make_bom(item=fg_item, raw_materials=[rm_item], do_not_submit=True) + for row in bom.items: + self.assertEqual(row.stock_uom, "Nos") + + frappe.db.set_value("Item", rm_item, "stock_uom", "Kg") + + bom.items[0].qty = 2 + bom.save() + + for row in bom.items: + self.assertEqual(row.stock_uom, "Kg") + def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.json b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json index 9d2384e31a0e..65664be2bead 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.json +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json @@ -280,6 +280,7 @@ "read_only": 1 } ], + "hide_toolbar": 1, "icon": "fa fa-sitemap", "is_submittable": 1, "links": [ @@ -288,7 +289,7 @@ "link_fieldname": "bom_creator" } ], - "modified": "2024-04-02 16:30:59.779190", + "modified": "2024-09-21 09:05:52.945112", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Creator", 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 12d52ea51fd8..34a3900015d6 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -8,7 +8,7 @@ from frappe.model.document import Document from frappe.query_builder import DocType, Interval from frappe.query_builder.functions import Now -from frappe.utils import cint, cstr +from frappe.utils import cint, cstr, date_diff, today from erpnext.manufacturing.doctype.bom_update_log.bom_updation_utils import ( get_leaf_boms, @@ -88,10 +88,12 @@ def validate_bom_cost_update_in_progress(self): wip_log = frappe.get_all( "BOM Update Log", - {"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress"]]}, + fields=["name", "modified"], + filters={"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress"]]}, limit_page_length=1, ) - if wip_log: + + if wip_log and date_diff(today(), wip_log[0].modified) < 1: 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), diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index 4cc60a3b4a6d..ad514efa55ff 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -37,7 +37,7 @@ frappe.ui.form.on("Job Card", { frappe.flags.resume_job = 0; let has_items = frm.doc.items && frm.doc.items.length; - if (!frm.is_new() && frm.doc.__onload.work_order_closed) { + if (!frm.is_new() && frm.doc.__onload?.work_order_closed) { frm.disable_save(); return; } diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index abea4c862790..586371090770 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -669,7 +669,7 @@ def on_cancel(self): self.set_transferred_qty() def validate_transfer_qty(self): - if self.items and self.transferred_qty < self.for_quantity: + if not self.is_corrective_job_card and self.items and self.transferred_qty < self.for_quantity: frappe.throw( _( "Materials needs to be transferred to the work in progress warehouse for the job card {0}" @@ -941,26 +941,19 @@ def set_transferred_qty(self, update_status=False): qty = 0 if self.work_order: - doc = frappe.get_doc("Work Order", self.work_order) if doc.transfer_material_against == "Job Card" and not doc.skip_transfer: - completed = True + min_qty = [] for d in doc.operations: - if d.status != "Completed": - completed = False + if d.completed_qty: + min_qty.append(d.completed_qty) + else: + min_qty = [] break - if completed: - job_cards = frappe.get_all( - "Job Card", - filters={"work_order": self.work_order, "docstatus": ("!=", 2)}, - fields="sum(transferred_qty) as qty", - group_by="operation_id", - ) - - if job_cards: - qty = min(d.qty for d in job_cards) + if min_qty: + qty = min(min_qty) - doc.db_set("material_transferred_for_manufacturing", qty) + doc.db_set("material_transferred_for_manufacturing", qty) self.set_status(update_status) diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json index 63e3fa3e9ff2..26cbc03eeb2b 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json @@ -5,18 +5,17 @@ "document_type": "Document", "engine": "InnoDB", "field_order": [ + "bom_and_work_order_tab", "raw_materials_consumption_section", "material_consumption", "get_rm_cost_from_consumption_entry", "column_break_3", "backflush_raw_materials_based_on", - "capacity_planning", - "disable_capacity_planning", - "allow_overtime", - "allow_production_on_holidays", - "column_break_5", - "capacity_planning_for_days", - "mins_between_operations", + "validate_components_quantities_per_bom", + "bom_section", + "update_bom_costs_automatically", + "column_break_lhyt", + "manufacture_sub_assembly_in_operation", "section_break_6", "default_wip_warehouse", "default_fg_warehouse", @@ -30,8 +29,14 @@ "add_corrective_operation_cost_in_finished_good_valuation", "column_break_24", "job_card_excess_transfer", + "capacity_planning", + "disable_capacity_planning", + "allow_overtime", + "allow_production_on_holidays", + "column_break_5", + "capacity_planning_for_days", + "mins_between_operations", "other_settings_section", - "update_bom_costs_automatically", "set_op_cost_and_scrape_from_sub_assemblies", "column_break_23", "make_serial_no_batch_from_work_order" @@ -149,7 +154,7 @@ { "fieldname": "raw_materials_consumption_section", "fieldtype": "Section Break", - "label": "Raw Materials Consumption" + "label": "Raw Materials Consumption " }, { "fieldname": "column_break_16", @@ -183,8 +188,8 @@ }, { "fieldname": "job_card_section", - "fieldtype": "Section Break", - "label": "Job Card" + "fieldtype": "Tab Break", + "label": "Job Card and Capacity Planning" }, { "fieldname": "column_break_24", @@ -210,13 +215,41 @@ "fieldname": "get_rm_cost_from_consumption_entry", "fieldtype": "Check", "label": "Get Raw Materials Cost from Consumption Entry" + }, + { + "fieldname": "bom_and_work_order_tab", + "fieldtype": "Tab Break", + "label": "BOM and Production" + }, + { + "fieldname": "bom_section", + "fieldtype": "Section Break", + "label": "BOM" + }, + { + "fieldname": "column_break_lhyt", + "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "If enabled then system will manufacture Sub-assembly against the Job Card (operation).", + "fieldname": "manufacture_sub_assembly_in_operation", + "fieldtype": "Check", + "label": "Manufacture Sub-assembly in Operation" + }, + { + "default": "0", + "depends_on": "eval:doc.backflush_raw_materials_based_on == \"BOM\"", + "fieldname": "validate_components_quantities_per_bom", + "fieldtype": "Check", + "label": "Validate Components Quantities Per BOM" } ], "icon": "icon-wrench", "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-02-08 19:00:37.561244", + "modified": "2024-09-02 12:12:03.132567", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing Settings", @@ -234,4 +267,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py index 84dbce2b83f0..cb2916c8ac59 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py @@ -29,15 +29,22 @@ class ManufacturingSettings(Document): get_rm_cost_from_consumption_entry: DF.Check job_card_excess_transfer: DF.Check make_serial_no_batch_from_work_order: DF.Check + manufacture_sub_assembly_in_operation: DF.Check material_consumption: DF.Check mins_between_operations: DF.Int overproduction_percentage_for_sales_order: DF.Percent overproduction_percentage_for_work_order: DF.Percent set_op_cost_and_scrape_from_sub_assemblies: DF.Check update_bom_costs_automatically: DF.Check + validate_components_quantities_per_bom: DF.Check # end: auto-generated types - pass + def before_save(self): + self.reset_values() + + def reset_values(self): + if self.backflush_raw_materials_based_on != "BOM" and self.validate_components_quantities_per_bom: + self.validate_components_quantities_per_bom = 0 def get_mins_between_operations(): diff --git a/erpnext/manufacturing/doctype/plant_floor/plant_floor.js b/erpnext/manufacturing/doctype/plant_floor/plant_floor.js index becdbdb9c95a..3d18900b70cb 100644 --- a/erpnext/manufacturing/doctype/plant_floor/plant_floor.js +++ b/erpnext/manufacturing/doctype/plant_floor/plant_floor.js @@ -6,6 +6,22 @@ frappe.ui.form.on("Plant Floor", { frm.trigger("setup_queries"); }, + add_workstation(frm) { + frm.add_custom_button(__("Create Workstation"), () => { + var doc = frappe.model.get_new_doc("Workstation"); + doc.plant_floor = frm.doc.name; + doc.status = "Off"; + frappe.ui.form.make_quick_entry( + "Workstation", + () => { + frm.trigger("prepare_workstation_dashboard"); + }, + null, + doc + ); + }).addClass("btn-primary"); + }, + setup_queries(frm) { frm.set_query("warehouse", (doc) => { if (!doc.company) { @@ -24,6 +40,11 @@ frappe.ui.form.on("Plant Floor", { refresh(frm) { frm.trigger("prepare_stock_dashboard"); frm.trigger("prepare_workstation_dashboard"); + + if (!frm.is_new()) { + frm.trigger("add_workstation"); + frm.disable_save(); + } }, prepare_workstation_dashboard(frm) { diff --git a/erpnext/manufacturing/doctype/plant_floor/plant_floor.json b/erpnext/manufacturing/doctype/plant_floor/plant_floor.json index be0052c47bf5..c1c167c395b6 100644 --- a/erpnext/manufacturing/doctype/plant_floor/plant_floor.json +++ b/erpnext/manufacturing/doctype/plant_floor/plant_floor.json @@ -69,9 +69,10 @@ "options": "Company" } ], + "hide_toolbar": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2024-01-30 11:59:07.508535", + "modified": "2024-09-19 19:06:36.481625", "modified_by": "Administrator", "module": "Manufacturing", "name": "Plant Floor", @@ -94,4 +95,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 5b4ef2339265..e7ac7f7cc833 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -87,17 +87,17 @@ frappe.ui.form.on("Production Plan", { if (frm.doc.docstatus === 1) { frm.trigger("show_progress"); - if (frm.doc.status !== "Completed") { - frm.add_custom_button( - __("Production Plan Summary"), - () => { - frappe.set_route("query-report", "Production Plan Summary", { - production_plan: frm.doc.name, - }); - }, - __("View") - ); + frm.add_custom_button( + __("Production Plan Summary"), + () => { + frappe.set_route("query-report", "Production Plan Summary", { + production_plan: frm.doc.name, + }); + }, + __("View") + ); + if (frm.doc.status !== "Completed") { if (frm.doc.status === "Closed") { frm.add_custom_button( __("Re-open"), @@ -277,7 +277,7 @@ frappe.ui.form.on("Production Plan", { frm.clear_table("prod_plan_references"); frappe.call({ - method: "get_items", + method: "combine_so_items", freeze: true, doc: frm.doc, callback: function () { diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 84bbad58c389..22971d4debdf 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -243,7 +243,7 @@ "depends_on": "eval:!doc.__islocal", "fieldname": "download_materials_required", "fieldtype": "Button", - "label": "Download Materials Request Plan" + "label": "Download Required Materials" }, { "fieldname": "get_items_for_mr", @@ -398,7 +398,7 @@ "collapsible": 1, "fieldname": "download_materials_request_plan_section_section", "fieldtype": "Section Break", - "label": "Download Materials Request Plan Section" + "label": "Preview Required Materials" }, { "default": "0", @@ -439,7 +439,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-02-27 13:34:20.692211", + "modified": "2024-12-04 11:55:03.108971", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", @@ -463,4 +463,4 @@ "sort_field": "modified", "sort_order": "ASC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 7d3aa000c870..265f99e47d3c 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -44,9 +44,7 @@ class ProductionPlan(Document): from erpnext.manufacturing.doctype.material_request_plan_item.material_request_plan_item import ( MaterialRequestPlanItem, ) - from erpnext.manufacturing.doctype.production_plan_item.production_plan_item import ( - ProductionPlanItem, - ) + from erpnext.manufacturing.doctype.production_plan_item.production_plan_item import ProductionPlanItem from erpnext.manufacturing.doctype.production_plan_item_reference.production_plan_item_reference import ( ProductionPlanItemReference, ) @@ -269,6 +267,31 @@ def add_mr_in_table(self, pending_mr): {"material_request": data.name, "material_request_date": data.transaction_date}, ) + @frappe.whitelist() + def combine_so_items(self): + if self.combine_items and self.po_items and len(self.po_items) > 0: + items = [] + for row in self.po_items: + items.append( + frappe._dict( + { + "parent": row.sales_order, + "item_code": row.item_code, + "warehouse": row.warehouse, + "qty": row.pending_qty, + "pending_qty": row.pending_qty, + "conversion_factor": 1.0, + "description": row.description, + "bom_no": row.bom_no, + } + ) + ) + + self.set("po_items", []) + self.add_items(items) + else: + self.get_items() + @frappe.whitelist() def get_items(self): self.set("po_items", []) @@ -435,24 +458,28 @@ def add_items(self, items): item_details = get_item_details(data.item_code, throw=False) if self.combine_items: - if item_details.bom_no in refs: - refs[item_details.bom_no]["so_details"].append( + bom_no = item_details.bom_no + if data.get("bom_no"): + bom_no = data.get("bom_no") + + if bom_no in refs: + refs[bom_no]["so_details"].append( {"sales_order": data.parent, "sales_order_item": data.name, "qty": data.pending_qty} ) - refs[item_details.bom_no]["qty"] += data.pending_qty + refs[bom_no]["qty"] += data.pending_qty continue else: - refs[item_details.bom_no] = { + refs[bom_no] = { "qty": data.pending_qty, "po_item_ref": data.name, "so_details": [], } - refs[item_details.bom_no]["so_details"].append( + refs[bom_no]["so_details"].append( {"sales_order": data.parent, "sales_order_item": data.name, "qty": data.pending_qty} ) - bom_no = data.bom_no or item_details and item_details.bom_no or "" + bom_no = data.bom_no or item_details and item_details.get("bom_no") or "" if not bom_no: continue @@ -1056,24 +1083,33 @@ def download_raw_materials(doc, warehouses=None): frappe.flags.show_qty_in_stock_uom = 1 items = get_items_for_material_requests(doc, warehouses=warehouses, get_parent_warehouse_data=True) + duplicate_item_wh_list = frappe._dict() + for d in items: - item_list.append( - [ - d.get("item_code"), - d.get("item_name"), - d.get("description"), - d.get("stock_uom"), - d.get("warehouse"), - d.get("required_bom_qty"), - d.get("projected_qty"), - d.get("actual_qty"), - d.get("ordered_qty"), - d.get("planned_qty"), - d.get("reserved_qty_for_production"), - d.get("safety_stock"), - d.get("quantity"), - ] - ) + key = (d.get("item_code"), d.get("warehouse")) + if key in duplicate_item_wh_list: + rm_data = duplicate_item_wh_list[key] + rm_data[12] += d.get("quantity") + continue + + rm_data = [ + d.get("item_code"), + d.get("item_name"), + d.get("description"), + d.get("stock_uom"), + d.get("warehouse"), + d.get("required_bom_qty"), + d.get("projected_qty"), + d.get("actual_qty"), + d.get("ordered_qty"), + d.get("planned_qty"), + d.get("reserved_qty_for_production"), + d.get("safety_stock"), + d.get("quantity"), + ] + + duplicate_item_wh_list[key] = rm_data + item_list.append(rm_data) if not doc.get("for_warehouse"): row = {"item_code": d.get("item_code")} diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index d1f2cdbd1aa4..6ee3f73eb084 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -1531,6 +1531,197 @@ def test_backflushed_serial_no_batch_raw_materials_based_on_transferred(self): self.assertFalse(serial_nos) + def test_backflushed_batch_raw_materials_based_on_transferred_autosabb(self): + frappe.db.set_single_value( + "Manufacturing Settings", + "backflush_raw_materials_based_on", + "Material Transferred for Manufacture", + ) + + batch_item = "Test Batch MCC Keyboard" + fg_item = "Test FG Item with Batch Raw Materials" + + ste_doc = test_stock_entry.make_stock_entry( + item_code=batch_item, target="Stores - _TC", qty=8, basic_rate=100, do_not_save=True + ) + + # Inward raw materials in Stores warehouse + ste_doc.submit() + ste_doc.reload() + + batch_no = get_batch_from_bundle(ste_doc.items[0].serial_and_batch_bundle) + + wo_doc = make_wo_order_test_record(production_item=fg_item, qty=4) + # action taken upon Start button: + transferred_ste_doc = frappe.get_doc( + make_stock_entry(wo_doc.name, "Material Transfer for Manufacture", 4) + ) + + transferred_ste_doc.submit() + transferred_ste_doc.reload() + + self.assertTrue(transferred_ste_doc.items[0].serial_and_batch_bundle) + self.assertEqual( + get_batch_from_bundle(transferred_ste_doc.items[0].serial_and_batch_bundle), batch_no + ) + self.assertEqual(transferred_ste_doc.items[0].qty, 4.0) + + # Make additional consumption and link to WO + test_stock_entry.make_stock_entry( + item_code="Test Batch Battery Consumable", + target="Stores - _TC", + qty=8, + basic_rate=2.33, + ) + consume_use_doc = test_stock_entry.make_stock_entry( + item_code="Test Batch Battery Consumable", # consumable not linked to BOM + source="Stores - _TC", + qty=4, + purpose="Material Consumption for Manufacture", + do_not_save=True, + ) + consume_use_doc.work_order = wo_doc.name + consume_use_doc.fg_completed_qty = 4 + consume_use_doc.submit() + consume_use_doc.reload() + + manufacture_ste_doc = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 4)) + mfr_items = [i.as_dict() for i in manufacture_ste_doc.items] + manufacture_ste_doc.submit() + manufacture_ste_doc.reload() + + self.assertTrue(len(mfr_items), 2) + self.assertTrue(manufacture_ste_doc.items[0].serial_and_batch_bundle) + self.assertEqual( + get_batch_from_bundle(manufacture_ste_doc.items[0].serial_and_batch_bundle), batch_no + ) + self.assertEqual(manufacture_ste_doc.items[0].qty, 4.0) + + def test_backflushed_serial_no_raw_materials_based_on_transferred_autosabb(self): + frappe.db.set_single_value( + "Manufacturing Settings", + "backflush_raw_materials_based_on", + "Material Transferred for Manufacture", + ) + + sn_item = "Test Serial No BTT Headphone" + fg_item = "Test FG Item with Serial No Raw Materials" + + ste_doc = test_stock_entry.make_stock_entry( + item_code=sn_item, target="Stores - _TC", qty=4, basic_rate=100, do_not_save=True + ) + + # Inward raw materials in Stores warehouse + ste_doc.submit() + ste_doc.reload() + + serial_nos_list = sorted(get_serial_nos_from_bundle(ste_doc.items[0].serial_and_batch_bundle)) + + wo_doc = make_wo_order_test_record(production_item=fg_item, qty=4) + transferred_ste_doc = frappe.get_doc( + make_stock_entry(wo_doc.name, "Material Transfer for Manufacture", 4) + ) + + transferred_ste_doc.submit() + transferred_ste_doc.reload() + + self.assertTrue(transferred_ste_doc.items[0].serial_and_batch_bundle) + self.assertEqual( + sorted(get_serial_nos_from_bundle(transferred_ste_doc.items[0].serial_and_batch_bundle)), + serial_nos_list, + ) + self.assertEqual(transferred_ste_doc.items[0].qty, 4.0) + + # Make additional consumption and link to WO + test_stock_entry.make_stock_entry( + item_code="Test Serial Battery Consumable", + target="Stores - _TC", + qty=8, + basic_rate=3.33, + ) + consume_use_doc = test_stock_entry.make_stock_entry( + item_code="Test Serial Battery Consumable", # consumable not linked to BOM + source="Stores - _TC", + qty=4, + purpose="Material Consumption for Manufacture", + do_not_save=True, + ) + consume_use_doc.work_order = wo_doc.name + consume_use_doc.fg_completed_qty = 4 + consume_use_doc.submit() + consume_use_doc.reload() + + manufacture_ste_doc = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 4)) + mfr_items = [i.as_dict() for i in manufacture_ste_doc.items] + manufacture_ste_doc.submit() + manufacture_ste_doc.reload() + + self.assertTrue(len(mfr_items), 2) + self.assertTrue(manufacture_ste_doc.items[0].serial_and_batch_bundle) + self.assertEqual( + sorted(get_serial_nos_from_bundle(manufacture_ste_doc.items[0].serial_and_batch_bundle)), + serial_nos_list, + ) + self.assertEqual(manufacture_ste_doc.items[0].qty, 4.0) + + def test_backflushed_serial_no_batch_raw_materials_based_on_transferred_autosabb(self): + frappe.db.set_single_value( + "Manufacturing Settings", + "backflush_raw_materials_based_on", + "Material Transferred for Manufacture", + ) + + sn_batch_item = "Test Batch Serial No WebCam" + fg_item = "Test FG Item with Serial & Batch No Raw Materials" + + ste_doc = test_stock_entry.make_stock_entry( + item_code=sn_batch_item, target="Stores - _TC", qty=4, basic_rate=100, do_not_save=True + ) + + ste_doc.submit() + ste_doc.reload() + + serial_nos_list = sorted(get_serial_nos_from_bundle(ste_doc.items[0].serial_and_batch_bundle)) + batch_no = get_batch_from_bundle(ste_doc.items[0].serial_and_batch_bundle) + + wo_doc = make_wo_order_test_record(production_item=fg_item, qty=4) + transferred_ste_doc = frappe.get_doc( + make_stock_entry(wo_doc.name, "Material Transfer for Manufacture", 4) + ) + + transferred_ste_doc.submit() + transferred_ste_doc.reload() + + self.assertTrue(transferred_ste_doc.items[0].serial_and_batch_bundle) + self.assertEqual( + sorted(get_serial_nos_from_bundle(transferred_ste_doc.items[0].serial_and_batch_bundle)), + serial_nos_list, + ) + self.assertEqual( + get_batch_from_bundle(transferred_ste_doc.items[0].serial_and_batch_bundle), batch_no + ) + self.assertEqual(transferred_ste_doc.items[0].qty, 4.0) + + manufacture_ste_doc = frappe.get_doc(make_stock_entry(wo_doc.name, "Manufacture", 4)) + manufacture_ste_doc.submit() + manufacture_ste_doc.reload() + + self.assertTrue(manufacture_ste_doc.items[0].serial_and_batch_bundle) + self.assertEqual( + sorted(get_serial_nos_from_bundle(manufacture_ste_doc.items[0].serial_and_batch_bundle)), + serial_nos_list, + ) + self.assertEqual( + get_batch_from_bundle(manufacture_ste_doc.items[0].serial_and_batch_bundle), batch_no + ) + self.assertEqual(manufacture_ste_doc.items[0].qty, 4.0) + + bundle = manufacture_ste_doc.items[0].serial_and_batch_bundle + bundle_doc = frappe.get_doc("Serial and Batch Bundle", bundle) + qty = sum(e.qty for e in bundle_doc.entries) + self.assertEqual(qty, -4.0) + + ### def test_non_consumed_material_return_against_work_order(self): frappe.db.set_single_value( "Manufacturing Settings", @@ -2053,6 +2244,108 @@ def test_partial_material_consumption_with_batch(self): "BOM", ) + def test_disassemby_order(self): + fg_item = "Test Disassembly Item" + source_warehouse = "Stores - _TC" + raw_materials = ["Test Disassembly RM Item 1", "Test Disassembly RM Item 2"] + + make_item(fg_item, {"is_stock_item": 1}) + for item in raw_materials: + make_item(item, {"is_stock_item": 1}) + test_stock_entry.make_stock_entry( + item_code=item, + target=source_warehouse, + qty=1, + basic_rate=100, + ) + + make_bom(item=fg_item, source_warehouse=source_warehouse, raw_materials=raw_materials) + + wo = make_wo_order_test_record( + item=fg_item, + qty=1, + source_warehouse=source_warehouse, + skip_transfer=1, + ) + + stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 1)) + for row in stock_entry.items: + if row.item_code in raw_materials: + row.s_warehouse = source_warehouse + + stock_entry.submit() + + wo.reload() + self.assertEqual(wo.status, "Completed") + + stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Disassemble", 1)) + stock_entry.save() + + self.assertEqual(stock_entry.purpose, "Disassemble") + + for row in stock_entry.items: + if row.item_code == fg_item: + self.assertTrue(row.s_warehouse) + self.assertFalse(row.t_warehouse) + else: + self.assertFalse(row.s_warehouse) + self.assertTrue(row.t_warehouse) + + stock_entry.submit() + + def test_components_qty_for_bom_based_manufacture_entry(self): + frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM") + frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 1) + + fg_item = "Test FG Item For Component Validation" + source_warehouse = "Stores - _TC" + raw_materials = ["Test Component Validation RM Item 1", "Test Component Validation RM Item 2"] + + make_item(fg_item, {"is_stock_item": 1}) + for item in raw_materials: + make_item(item, {"is_stock_item": 1}) + test_stock_entry.make_stock_entry( + item_code=item, + target=source_warehouse, + qty=10, + basic_rate=100, + ) + + make_bom(item=fg_item, source_warehouse=source_warehouse, raw_materials=raw_materials) + + wo = make_wo_order_test_record( + item=fg_item, + qty=10, + source_warehouse=source_warehouse, + ) + + transfer_entry = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 10)) + transfer_entry.save() + for row in transfer_entry.items: + row.qty = 5 + + self.assertRaises(frappe.ValidationError, transfer_entry.save) + + transfer_entry.reload() + for row in transfer_entry.items: + self.assertEqual(row.qty, 10) + + transfer_entry.submit() + + manufacture_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 10)) + manufacture_entry.save() + for row in manufacture_entry.items: + if not row.s_warehouse: + continue + + row.qty = 5 + + self.assertRaises(frappe.ValidationError, manufacture_entry.save) + manufacture_entry.reload() + manufacture_entry.submit() + + frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 0) + def make_operation(**kwargs): kwargs = frappe._dict(kwargs) @@ -2233,6 +2526,29 @@ def prepare_data_for_backflush_based_on_materials_transferred(): make_bom(item=item.name, source_warehouse="Stores - _TC", raw_materials=[batch_item_doc.name]) + # Make additional items not attached to a BOM + make_item( + "Test Batch Battery Consumable", + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TBMK.#####", + "valuation_rate": 2.33, + "stock_uom": "Nos", + }, + ) + make_item( + "Test Serial Battery Consumable", + { + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "TSBH.#####", + "valuation_rate": 3.33, + "stock_uom": "Nos", + }, + ) + sn_item_doc = make_item( "Test Serial No BTT Headphone", { @@ -2370,6 +2686,7 @@ def make_wo_order_test_record(**args): wo_order.batch_size = args.batch_size or 0 if args.source_warehouse: + wo_order.source_warehouse = args.source_warehouse for item in wo_order.get("required_items"): item.source_warehouse = args.source_warehouse diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 1da33f0ad9b1..df72b1e6b510 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -183,13 +183,30 @@ frappe.ui.form.on("Work Order", { } } + if (frm.doc.status == "Completed") { + if (frm.doc.__onload.backflush_raw_materials_based_on == "Material Transferred for Manufacture") { + frm.add_custom_button( + __("BOM"), + () => { + frm.trigger("make_bom"); + }, + __("Create") + ); + } + } + if ( - frm.doc.status == "Completed" && - frm.doc.__onload.backflush_raw_materials_based_on == "Material Transferred for Manufacture" + frm.doc.docstatus === 1 && + ["Closed", "Completed"].includes(frm.doc.status) && + frm.doc.produced_qty > 0 ) { - frm.add_custom_button(__("Create BOM"), () => { - frm.trigger("make_bom"); - }); + frm.add_custom_button( + __("Disassemble Order"), + () => { + frm.trigger("make_disassembly_order"); + }, + __("Create") + ); } frm.trigger("add_custom_button_to_return_components"); @@ -337,6 +354,23 @@ frappe.ui.form.on("Work Order", { }); }, + make_disassembly_order(frm) { + erpnext.work_order + .show_prompt_for_qty_input(frm, "Disassemble") + .then((data) => { + return frappe.xcall("erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", { + work_order_id: frm.doc.name, + purpose: "Disassemble", + qty: data.qty, + target_warehouse: data.target_warehouse, + }); + }) + .then((stock_entry) => { + frappe.model.sync(stock_entry); + frappe.set_route("Form", stock_entry.doctype, stock_entry.name); + }); + }, + show_progress_for_items: function (frm) { var bars = []; var message = ""; @@ -745,6 +779,10 @@ erpnext.work_order = { get_max_transferable_qty: (frm, purpose) => { let max = 0; + if (purpose === "Disassemble") { + return flt(frm.doc.produced_qty); + } + if (frm.doc.skip_transfer) { max = flt(frm.doc.qty) - flt(frm.doc.produced_qty); } else { @@ -759,15 +797,38 @@ erpnext.work_order = { show_prompt_for_qty_input: function (frm, purpose) { let max = this.get_max_transferable_qty(frm, purpose); + + let fields = [ + { + fieldtype: "Float", + label: __("Qty for {0}", [__(purpose)]), + fieldname: "qty", + description: __("Max: {0}", [max]), + default: max, + }, + ]; + + if (purpose === "Disassemble") { + fields.push({ + fieldtype: "Link", + options: "Warehouse", + fieldname: "target_warehouse", + label: __("Target Warehouse"), + default: frm.doc.source_warehouse || frm.doc.wip_warehouse, + get_query() { + return { + filters: { + company: frm.doc.company, + is_group: 0, + }, + }; + }, + }); + } + return new Promise((resolve, reject) => { frappe.prompt( - { - fieldtype: "Float", - label: __("Qty for {0}", [__(purpose)]), - fieldname: "qty", - description: __("Max: {0}", [max]), - default: max, - }, + fields, (data) => { max += (frm.doc.qty * (frm.doc.__onload.overproduction_percentage || 0.0)) / 100; diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 904e42a6a035..9af3403ffa36 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -158,16 +158,39 @@ def validate(self): self.validate_operation_time() self.status = self.get_status() self.validate_workstation_type() + self.reset_use_multi_level_bom() + + if self.source_warehouse: + self.set_warehouses() validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"]) self.set_required_items(reset_only_qty=len(self.get("required_items"))) + def set_warehouses(self): + for row in self.required_items: + if not row.source_warehouse: + row.source_warehouse = self.source_warehouse + + def reset_use_multi_level_bom(self): + if self.is_new(): + return + + before_save_obj = self.get_doc_before_save() + if before_save_obj.use_multi_level_bom != self.use_multi_level_bom: + self.get_items_and_operations_from_bom() + def validate_workstation_type(self): + if not self.docstatus.is_submitted(): + return + for row in self.operations: if not row.workstation and not row.workstation_type: - msg = f"Row {row.idx}: Workstation or Workstation Type is mandatory for an operation {row.operation}" - frappe.throw(_(msg)) + frappe.throw( + _("Row {0}: Workstation or Workstation Type is mandatory for an operation {1}").format( + row.idx, row.operation + ) + ) def validate_sales_order(self): if self.sales_order: @@ -520,7 +543,6 @@ def create_batch_for_finished_good(self): def delete_auto_created_batch_and_serial_no(self): for row in frappe.get_all("Serial No", filters={"work_order": self.name}): frappe.delete_doc("Serial No", row.name) - self.db_set("serial_no", "") for row in frappe.get_all("Batch", filters={"reference_name": self.name}): frappe.delete_doc("Batch", row.name) @@ -1277,7 +1299,7 @@ def get_item_details(item, project=None, skip_bom_info=False, throw=True): @frappe.whitelist() -def make_work_order(bom_no, item, qty=0, project=None, variant_items=None): +def make_work_order(bom_no, item, qty=0, project=None, variant_items=None, use_multi_level_bom=None): if not frappe.has_permission("Work Order", "write"): frappe.throw(_("Not permitted"), frappe.PermissionError) @@ -1287,12 +1309,13 @@ def make_work_order(bom_no, item, qty=0, project=None, variant_items=None): wo_doc.production_item = item wo_doc.update(item_details) wo_doc.bom_no = bom_no + wo_doc.use_multi_level_bom = cint(use_multi_level_bom) if flt(qty) > 0: wo_doc.qty = flt(qty) wo_doc.get_items_and_operations_from_bom() - if variant_items: + if variant_items and not wo_doc.use_multi_level_bom: add_variant_item(variant_items, wo_doc, bom_no, "required_items") return wo_doc @@ -1336,7 +1359,20 @@ def add_variant_item(variant_items, wo_doc, bom_no, table_name="items"): args["amount"] = flt(args.get("required_qty")) * flt(args.get("rate")) args["uom"] = item_data.stock_uom - wo_doc.append(table_name, args) + + existing_row = ( + get_template_rm_item(wo_doc, item.get("item_code")) if table_name == "required_items" else None + ) + if existing_row: + existing_row.update(args) + else: + wo_doc.append(table_name, args) + + +def get_template_rm_item(wo_doc, item_code): + for row in wo_doc.required_items: + if row.item_code == item_code: + return row @frappe.whitelist() @@ -1359,7 +1395,7 @@ def set_work_order_ops(name): @frappe.whitelist() -def make_stock_entry(work_order_id, purpose, qty=None): +def make_stock_entry(work_order_id, purpose, qty=None, target_warehouse=None): work_order = frappe.get_doc("Work Order", work_order_id) if not frappe.db.get_value("Warehouse", work_order.wip_warehouse, "is_group"): wip_warehouse = work_order.wip_warehouse @@ -1389,9 +1425,16 @@ def make_stock_entry(work_order_id, purpose, qty=None): stock_entry.to_warehouse = work_order.fg_warehouse stock_entry.project = work_order.project + if purpose == "Disassemble": + stock_entry.from_warehouse = work_order.fg_warehouse + stock_entry.to_warehouse = target_warehouse or work_order.source_warehouse + stock_entry.set_stock_entry_type() stock_entry.get_items() - stock_entry.set_serial_no_batch_for_finished_good() + + if purpose != "Disassemble": + stock_entry.set_serial_no_batch_for_finished_good() + return stock_entry.as_dict() diff --git a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json index 0f4d693544eb..580168180a73 100644 --- a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json +++ b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json @@ -15,6 +15,7 @@ "include_item_in_manufacturing", "qty_section", "required_qty", + "stock_uom", "rate", "amount", "column_break_11", @@ -138,11 +139,19 @@ "in_list_view": 1, "label": "Returned Qty ", "read_only": 1 + }, + { + "fetch_from": "item_code.stock_uom", + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2024-02-11 15:45:32.318374", + "modified": "2024-11-19 15:48:16.823384", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Item", @@ -153,4 +162,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/work_order_item/work_order_item.py b/erpnext/manufacturing/doctype/work_order_item/work_order_item.py index 267ca5d21dee..04f78eb1c3b2 100644 --- a/erpnext/manufacturing/doctype/work_order_item/work_order_item.py +++ b/erpnext/manufacturing/doctype/work_order_item/work_order_item.py @@ -25,6 +25,7 @@ class WorkOrderItem(Document): item_code: DF.Link | None item_name: DF.Data | None operation: DF.Link | None + operation_row_id: DF.Int parent: DF.Data parentfield: DF.Data parenttype: DF.Data @@ -32,6 +33,7 @@ class WorkOrderItem(Document): required_qty: DF.Float returned_qty: DF.Float source_warehouse: DF.Link | None + stock_uom: DF.Link | None transferred_qty: DF.Float # end: auto-generated types diff --git a/erpnext/manufacturing/number_card/monthly_completed_work_order/monthly_completed_work_order.json b/erpnext/manufacturing/number_card/monthly_completed_work_order/monthly_completed_work_order.json index 36c0b9ae75b6..7381de707d78 100644 --- a/erpnext/manufacturing/number_card/monthly_completed_work_order/monthly_completed_work_order.json +++ b/erpnext/manufacturing/number_card/monthly_completed_work_order/monthly_completed_work_order.json @@ -3,6 +3,7 @@ "docstatus": 0, "doctype": "Number Card", "document_type": "Work Order", + "dynamic_filters_json": "[[\"Work Order\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", "filters_json": "[[\"Work Order\",\"status\",\"=\",\"Completed\"],[\"Work Order\",\"docstatus\",\"=\",1],[\"Work Order\",\"creation\",\"between\",[\"2020-06-08\",\"2020-07-08\"]]]", "function": "Count", "idx": 0, diff --git a/erpnext/manufacturing/number_card/monthly_total_work_order/monthly_total_work_order.json b/erpnext/manufacturing/number_card/monthly_total_work_order/monthly_total_work_order.json index 80d3b1520aed..e4b8c9404540 100644 --- a/erpnext/manufacturing/number_card/monthly_total_work_order/monthly_total_work_order.json +++ b/erpnext/manufacturing/number_card/monthly_total_work_order/monthly_total_work_order.json @@ -3,6 +3,7 @@ "docstatus": 0, "doctype": "Number Card", "document_type": "Work Order", + "dynamic_filters_json": "[[\"Work Order\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", "filters_json": "[[\"Work Order\",\"docstatus\",\"=\",1],[\"Work Order\",\"creation\",\"between\",[\"2020-06-08\",\"2020-07-08\"]]]", "function": "Count", "idx": 0, diff --git a/erpnext/manufacturing/number_card/ongoing_job_card/ongoing_job_card.json b/erpnext/manufacturing/number_card/ongoing_job_card/ongoing_job_card.json index ba23ff345318..0138af827a62 100644 --- a/erpnext/manufacturing/number_card/ongoing_job_card/ongoing_job_card.json +++ b/erpnext/manufacturing/number_card/ongoing_job_card/ongoing_job_card.json @@ -3,6 +3,7 @@ "docstatus": 0, "doctype": "Number Card", "document_type": "Job Card", + "dynamic_filters_json": "[[\"Job Card\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", "filters_json": "[[\"Job Card\",\"status\",\"!=\",\"Completed\"],[\"Job Card\",\"docstatus\",\"=\",1]]", "function": "Count", "idx": 0, diff --git a/erpnext/manufacturing/report/production_analytics/production_analytics.py b/erpnext/manufacturing/report/production_analytics/production_analytics.py index c02c1e6fcd38..dc2b9ad62f36 100644 --- a/erpnext/manufacturing/report/production_analytics/production_analytics.py +++ b/erpnext/manufacturing/report/production_analytics/production_analytics.py @@ -131,11 +131,11 @@ def get_chart_data(periodic_data, columns): pending.append(periodic_data.get("Pending").get(d)) completed.append(periodic_data.get("Completed").get(d)) - datasets.append({"name": "All Work Orders", "values": all_data}) - datasets.append({"name": "Not Started", "values": not_start}) - datasets.append({"name": "Overdue", "values": overdue}) - datasets.append({"name": "Pending", "values": pending}) - datasets.append({"name": "Completed", "values": completed}) + datasets.append({"name": _("All Work Orders"), "values": all_data}) + datasets.append({"name": _("Not Started"), "values": not_start}) + datasets.append({"name": _("Overdue"), "values": overdue}) + datasets.append({"name": _("Pending"), "values": pending}) + datasets.append({"name": _("Completed"), "values": completed}) chart = {"data": {"labels": labels, "datasets": datasets}} chart["type"] = "line" diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py index 5bc9236c1d5c..c62cab77d613 100644 --- a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py +++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py @@ -27,32 +27,51 @@ def get_data(filters): def get_production_plan_item_details(filters, data, order_details): - itemwise_indent = {} - production_plan_doc = frappe.get_cached_doc("Production Plan", filters.get("production_plan")) for row in production_plan_doc.po_items: - work_order = frappe.get_value( + work_orders = frappe.get_all( "Work Order", - {"production_plan_item": row.name, "bom_no": row.bom_no, "production_item": row.item_code}, - "name", + filters={ + "production_plan_item": row.name, + "bom_no": row.bom_no, + "production_item": row.item_code, + }, + pluck="name", ) - if row.item_code not in itemwise_indent: - itemwise_indent.setdefault(row.item_code, {}) + order_qty = row.planned_qty + total_produced_qty = 0.0 + pending_qty = 0.0 + for work_order in work_orders: + produced_qty = flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0)) + pending_qty = flt(order_qty) - produced_qty + + total_produced_qty += produced_qty + + data.append( + { + "indent": 0, + "item_code": row.item_code, + "sales_order": row.get("sales_order"), + "item_name": frappe.get_cached_value("Item", row.item_code, "item_name"), + "qty": order_qty, + "document_type": "Work Order", + "document_name": work_order or "", + "bom_level": 0, + "produced_qty": produced_qty, + "pending_qty": pending_qty, + } + ) + + order_qty = pending_qty data.append( { - "indent": 0, "item_code": row.item_code, - "sales_order": row.get("sales_order"), - "item_name": frappe.get_cached_value("Item", row.item_code, "item_name"), + "indent": 0, "qty": row.planned_qty, - "document_type": "Work Order", - "document_name": work_order or "", - "bom_level": 0, - "produced_qty": order_details.get((work_order, row.item_code), {}).get("produced_qty", 0), - "pending_qty": flt(row.planned_qty) - - flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0)), + "produced_qty": total_produced_qty, + "pending_qty": pending_qty, } ) 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 38e05852ee8d..7e0fcf14cc63 100644 --- a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py +++ b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py @@ -42,7 +42,7 @@ def get_data(filters): def get_chart_data(periodic_data, columns): - labels = ["Rejected", "Accepted"] + labels = [_("Rejected"), _("Accepted")] status_wise_data = {"Accepted": 0, "Rejected": 0} @@ -53,7 +53,7 @@ def get_chart_data(periodic_data, columns): datasets.append( { - "name": "Qty Wise Chart", + "name": _("Qty Wise Chart"), "values": [status_wise_data.get("Rejected"), status_wise_data.get("Accepted")], } ) diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json index d2520d6b7eb6..06b9f1b07597 100644 --- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json +++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json @@ -1,4 +1,5 @@ { + "app": "erpnext", "charts": [], "content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"YHCQG3wAGv\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Creator\",\"col\":3}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"Ubj6zXcmIQ\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Plant Floor\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"PIQJYZOMnD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Manufacturing\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", "creation": "2020-03-02 17:11:37.032604", @@ -124,11 +125,86 @@ "onboard": 0, "type": "Link" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Bill of Materials", + "link_count": 6, + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Item", + "link_count": 0, + "link_to": "Item", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "Item", + "hidden": 0, + "is_query_report": 0, + "label": "Bill of Materials", + "link_count": 0, + "link_to": "BOM", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Workstation Type", + "link_count": 0, + "link_to": "Workstation Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Workstation", + "link_count": 0, + "link_to": "Workstation", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Operation", + "link_count": 0, + "link_to": "Operation", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Work Order", + "hidden": 0, + "is_query_report": 1, + "label": "Routing", + "link_count": 0, + "link_to": "Routing", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "hidden": 0, "is_query_report": 0, "label": "Reports", "link_count": 10, + "link_type": "DocType", "onboard": 0, "type": "Card Break" }, @@ -233,90 +309,16 @@ }, { "hidden": 0, - "is_query_report": 0, + "is_query_report": 1, "label": "Work Order Consumed Materials", "link_count": 0, "link_to": "Work Order Consumed Materials", "link_type": "Report", "onboard": 0, "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Bill of Materials", - "link_count": 6, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Item", - "link_count": 0, - "link_to": "Item", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "Item", - "hidden": 0, - "is_query_report": 0, - "label": "Bill of Materials", - "link_count": 0, - "link_to": "BOM", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Workstation Type", - "link_count": 0, - "link_to": "Workstation Type", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Workstation", - "link_count": 0, - "link_to": "Workstation", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Operation", - "link_count": 0, - "link_to": "Operation", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "Work Order", - "hidden": 0, - "is_query_report": 1, - "label": "Routing", - "link_count": 0, - "link_to": "Routing", - "link_type": "DocType", - "onboard": 0, - "type": "Link" } ], - "modified": "2024-01-30 21:49:58.577218", + "modified": "2024-10-21 14:13:38.777556", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing", @@ -398,5 +400,6 @@ "type": "Report" } ], - "title": "Manufacturing" + "title": "Manufacturing", + "type": "Workspace" } \ No newline at end of file diff --git a/erpnext/modules.txt b/erpnext/modules.txt index c53cdf467d2f..b8b12e90fb08 100644 --- a/erpnext/modules.txt +++ b/erpnext/modules.txt @@ -18,3 +18,4 @@ Communication Telephony Bulk Transaction Subcontracting +EDI \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 208c9e58d4fd..a916478d4b37 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -314,7 +314,8 @@ erpnext.patches.v13_0.update_docs_link erpnext.patches.v15_0.update_asset_value_for_manual_depr_entries erpnext.patches.v15_0.update_gpa_and_ndb_for_assdeprsch erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance -erpnext.patches.v14_0.update_closing_balances #14-07-2023 +erpnext.patches.v14_0.set_period_start_end_date_in_pcv +erpnext.patches.v14_0.update_closing_balances #08-11-2024 execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0) erpnext.patches.v14_0.update_reference_type_in_journal_entry_accounts erpnext.patches.v14_0.update_subscription_details @@ -358,6 +359,7 @@ erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes erpnext.patches.v14_0.update_flag_for_return_invoices #2024-03-22 erpnext.patches.v15_0.create_accounting_dimensions_in_payment_request erpnext.patches.v14_0.update_pos_return_ledger_entries #2024-08-16 +erpnext.patches.v15_0.create_advance_payment_ledger_records # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20 @@ -371,3 +373,15 @@ erpnext.patches.v15_0.update_warehouse_field_in_asset_repair_consumed_item_docty erpnext.patches.v15_0.update_asset_repair_field_in_stock_entry erpnext.patches.v15_0.update_total_number_of_booked_depreciations erpnext.patches.v15_0.do_not_use_batchwise_valuation +erpnext.patches.v15_0.update_invoice_remarks +erpnext.patches.v14_0.update_reports_with_range +erpnext.patches.v15_0.drop_index_posting_datetime_from_sle +erpnext.patches.v15_0.add_disassembly_order_stock_entry_type #1 +erpnext.patches.v15_0.set_standard_stock_entry_type +erpnext.patches.v15_0.link_purchase_item_to_asset_doc +erpnext.patches.v14_0.update_currency_exchange_settings_for_frankfurter +erpnext.patches.v15_0.update_task_assignee_email_field_in_asset_maintenance_log +erpnext.patches.v15_0.update_sub_voucher_type_in_gl_entries +erpnext.patches.v14_0.update_stock_uom_in_work_order_item +erpnext.patches.v15_0.set_is_exchange_gain_loss_in_payment_entry_deductions +erpnext.patches.v15_0.enable_allow_existing_serial_no diff --git a/erpnext/patches/v13_0/fix_additional_cost_in_mfg_stock_entry.py b/erpnext/patches/v13_0/fix_additional_cost_in_mfg_stock_entry.py index e305b375c7fb..5609b6bb895e 100644 --- a/erpnext/patches/v13_0/fix_additional_cost_in_mfg_stock_entry.py +++ b/erpnext/patches/v13_0/fix_additional_cost_in_mfg_stock_entry.py @@ -15,7 +15,7 @@ def execute(): def find_broken_stock_entries() -> list[StockEntryCode]: period_closing_date = frappe.db.get_value( - "Period Closing Voucher", {"docstatus": 1}, "posting_date", order_by="posting_date desc" + "Period Closing Voucher", {"docstatus": 1}, "period_end_date", order_by="period_end_date desc" ) stock_entries_to_patch = frappe.db.sql( diff --git a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py index c0d715063a82..2441075de30f 100644 --- a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py +++ b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py @@ -38,7 +38,7 @@ def execute(): data = frappe.db.sql( """ SELECT - name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time, company + name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time, company, creation FROM `tabStock Ledger Entry` WHERE @@ -67,6 +67,7 @@ def execute(): "voucher_type": d.voucher_type, "voucher_no": d.voucher_no, "sle_id": d.name, + "creation": d.creation, }, allow_negative_stock=True, ) diff --git a/erpnext/patches/v14_0/set_period_start_end_date_in_pcv.py b/erpnext/patches/v14_0/set_period_start_end_date_in_pcv.py new file mode 100644 index 000000000000..8020a286f69d --- /dev/null +++ b/erpnext/patches/v14_0/set_period_start_end_date_in_pcv.py @@ -0,0 +1,17 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# License: MIT. See LICENSE + + +import frappe + + +def execute(): + # nosemgrep + frappe.db.sql( + """ + UPDATE `tabPeriod Closing Voucher` + SET + period_start_date = (select year_start_date from `tabFiscal Year` where name = fiscal_year), + period_end_date = posting_date + """ + ) diff --git a/erpnext/patches/v14_0/single_to_multi_dunning.py b/erpnext/patches/v14_0/single_to_multi_dunning.py index 3b01871d4376..98be0204518c 100644 --- a/erpnext/patches/v14_0/single_to_multi_dunning.py +++ b/erpnext/patches/v14_0/single_to_multi_dunning.py @@ -66,7 +66,7 @@ def get_accounts_closing_date(): ) # always returns datetime.date period_closing_date = frappe.db.get_value( - "Period Closing Voucher", {"docstatus": 1}, "posting_date", order_by="posting_date desc" + "Period Closing Voucher", {"docstatus": 1}, "period_end_date", order_by="period_end_date desc" ) # Set most recent frozen/closing date as filter diff --git a/erpnext/patches/v14_0/update_closing_balances.py b/erpnext/patches/v14_0/update_closing_balances.py index cfc29c87fa11..31c7e85d875e 100644 --- a/erpnext/patches/v14_0/update_closing_balances.py +++ b/erpnext/patches/v14_0/update_closing_balances.py @@ -7,67 +7,83 @@ from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import ( make_closing_entries, ) +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + get_accounting_dimensions, +) from erpnext.accounts.utils import get_fiscal_year def execute(): frappe.db.truncate("Account Closing Balance") + gle_fields = get_gle_fields() + for company in frappe.get_all("Company", pluck="name"): i = 0 company_wise_order = {} - for pcv in frappe.db.get_all( - "Period Closing Voucher", - fields=["company", "posting_date", "name"], - filters={"docstatus": 1, "company": company}, - order_by="posting_date", - ): + for pcv in get_period_closing_vouchers(company): company_wise_order.setdefault(pcv.company, []) - if pcv.posting_date not in company_wise_order[pcv.company]: + if pcv.period_end_date not in company_wise_order[pcv.company]: pcv_doc = frappe.get_doc("Period Closing Voucher", pcv.name) - pcv_doc.year_start_date = get_fiscal_year( - pcv.posting_date, pcv.fiscal_year, company=pcv.company - )[1] - - # get gl entries against pcv - gl_entries = frappe.db.get_all( - "GL Entry", filters={"voucher_no": pcv.name, "is_cancelled": 0}, fields=["*"] - ) - for entry in gl_entries: - entry["is_period_closing_voucher_entry"] = 1 - entry["closing_date"] = pcv_doc.posting_date - entry["period_closing_voucher"] = pcv_doc.name - - closing_entries = [] - - if pcv.posting_date not in company_wise_order[pcv.company]: - # get all gl entries for the year - closing_entries = frappe.db.get_all( - "GL Entry", - filters={ - "is_cancelled": 0, - "voucher_no": ["!=", pcv.name], - "posting_date": ["between", [pcv_doc.year_start_date, pcv.posting_date]], - "is_opening": "No", - "company": company, - }, - fields=["*"], - ) - - if i == 0: - # add opening entries only for the first pcv - closing_entries += frappe.db.get_all( - "GL Entry", - filters={"is_cancelled": 0, "is_opening": "Yes", "company": company}, - fields=["*"], - ) - - for entry in closing_entries: - entry["closing_date"] = pcv_doc.posting_date - entry["period_closing_voucher"] = pcv_doc.name - - entries = gl_entries + closing_entries - - make_closing_entries(entries, pcv.name, pcv.company, pcv.posting_date) - company_wise_order[pcv.company].append(pcv.posting_date) + pcv_doc.pl_accounts_reverse_gle = get_pcv_gl_entries_for_pl_accounts(pcv, gle_fields) + pcv_doc.closing_account_gle = get_pcv_gl_entries_for_closing_accounts(pcv, gle_fields) + closing_entries = pcv_doc.get_account_closing_balances() + make_closing_entries(closing_entries, pcv.name, pcv.company, pcv.period_end_date) + + company_wise_order[pcv.company].append(pcv.period_end_date) i += 1 + + +def get_gle_fields(): + default_diemnsion_fields = ["cost_center", "finance_book", "project"] + accounting_dimension_fields = get_accounting_dimensions() + gle_fields = [ + "name", + "company", + "posting_date", + "account", + "account_currency", + "debit", + "credit", + "debit_in_account_currency", + "credit_in_account_currency", + *default_diemnsion_fields, + *accounting_dimension_fields, + ] + + return gle_fields + + +def get_period_closing_vouchers(company): + return frappe.db.get_all( + "Period Closing Voucher", + fields=["name", "closing_account_head", "period_start_date", "period_end_date", "company"], + filters={"docstatus": 1, "company": company}, + order_by="period_end_date", + ) + + +def get_pcv_gl_entries_for_pl_accounts(pcv, gle_fields): + return get_gl_entries(pcv, gle_fields, {"account": ["!=", pcv.closing_account_head]}) + + +def get_pcv_gl_entries_for_closing_accounts(pcv, gle_fields): + return get_gl_entries(pcv, gle_fields, {"account": pcv.closing_account_head}) + + +def get_gl_entries(pcv, gle_fields, accounts_filter=None): + filters = {"voucher_no": pcv.name, "is_cancelled": 0} + if accounts_filter: + filters.update(accounts_filter) + + gl_entries = frappe.db.get_all( + "GL Entry", + filters=filters, + fields=gle_fields, + ) + for entry in gl_entries: + entry["is_period_closing_voucher_entry"] = 1 + entry["closing_date"] = pcv.period_end_date + entry["period_closing_voucher"] = pcv.name + + return gl_entries diff --git a/erpnext/patches/v14_0/update_currency_exchange_settings_for_frankfurter.py b/erpnext/patches/v14_0/update_currency_exchange_settings_for_frankfurter.py new file mode 100644 index 000000000000..a67c5a262377 --- /dev/null +++ b/erpnext/patches/v14_0/update_currency_exchange_settings_for_frankfurter.py @@ -0,0 +1,11 @@ +import frappe + + +def execute(): + settings = frappe.get_doc("Currency Exchange Settings") + if settings.service_provider != "frankfurter.app": + return + + settings.set_parameters_and_result() + settings.flags.ignore_validate = True + settings.save() diff --git a/erpnext/patches/v14_0/update_reports_with_range.py b/erpnext/patches/v14_0/update_reports_with_range.py new file mode 100644 index 000000000000..014fba883fce --- /dev/null +++ b/erpnext/patches/v14_0/update_reports_with_range.py @@ -0,0 +1,39 @@ +import json + +import frappe + +REFERENCE_REPORTS = [ + "Accounts Receivable", + "Accounts Receivable Summary", + "Accounts Payable", + "Accounts Payable Summary", + "Stock Ageing", +] + + +def execute(): + for report in REFERENCE_REPORTS: + update_reference_reports(report) + + +def update_reference_reports(reference_report): + reports = frappe.get_all( + "Report", filters={"reference_report": reference_report}, fields={"json", "name"} + ) + + for report in reports: + update_report_json(report) + update_reference_reports(report.name) + + +def update_report_json(report): + report_json = json.loads(report.json) + report_filter = report_json.get("filters") + + if not report_filter: + return + + keys_to_pop = [key for key in report_filter if key.startswith("range")] + report_filter["range"] = ", ".join(str(report_filter.pop(key)) for key in keys_to_pop) + + frappe.db.set_value("Report", report.name, "json", json.dumps(report_json)) diff --git a/erpnext/patches/v14_0/update_stock_uom_in_work_order_item.py b/erpnext/patches/v14_0/update_stock_uom_in_work_order_item.py new file mode 100644 index 000000000000..d611065d8f10 --- /dev/null +++ b/erpnext/patches/v14_0/update_stock_uom_in_work_order_item.py @@ -0,0 +1,15 @@ +import frappe + + +def execute(): + frappe.db.sql( + """ + UPDATE + `tabWork Order Item`, `tabItem` + SET + `tabWork Order Item`.stock_uom = `tabItem`.stock_uom + WHERE + `tabWork Order Item`.item_code = `tabItem`.name + AND `tabWork Order Item`.docstatus = 1 + """ + ) diff --git a/erpnext/patches/v15_0/add_disassembly_order_stock_entry_type.py b/erpnext/patches/v15_0/add_disassembly_order_stock_entry_type.py new file mode 100644 index 000000000000..1f3413b172b7 --- /dev/null +++ b/erpnext/patches/v15_0/add_disassembly_order_stock_entry_type.py @@ -0,0 +1,13 @@ +import frappe + + +def execute(): + if not frappe.db.exists("Stock Entry Type", "Disassemble"): + frappe.get_doc( + { + "doctype": "Stock Entry Type", + "name": "Disassemble", + "purpose": "Disassemble", + "is_standard": 1, + } + ).insert(ignore_permissions=True) diff --git a/erpnext/patches/v15_0/create_advance_payment_ledger_records.py b/erpnext/patches/v15_0/create_advance_payment_ledger_records.py new file mode 100644 index 000000000000..8d247885cab2 --- /dev/null +++ b/erpnext/patches/v15_0/create_advance_payment_ledger_records.py @@ -0,0 +1,73 @@ +import frappe +from frappe import qb +from frappe.query_builder.custom import ConstantColumn + + +def get_advance_doctypes() -> list: + return frappe.get_hooks("advance_payment_doctypes") + + +def get_payments_with_so_po_reference() -> list: + advance_payment_entries = [] + advance_doctypes = get_advance_doctypes() + per = qb.DocType("Payment Entry Reference") + payments_with_reference = ( + qb.from_(per) + .select(per.parent) + .distinct() + .where(per.reference_doctype.isin(advance_doctypes) & per.docstatus.eq(1)) + .run() + ) + if payments_with_reference: + pe = qb.DocType("Payment Entry") + advance_payment_entries = ( + qb.from_(pe) + .select(ConstantColumn("Payment Entry").as_("doctype")) + .select(pe.name) + .where(pe.name.isin(payments_with_reference) & pe.docstatus.eq(1)) + .run(as_dict=True) + ) + + return advance_payment_entries + + +def get_journals_with_so_po_reference() -> list: + advance_journal_entries = [] + advance_doctypes = get_advance_doctypes() + jea = qb.DocType("Journal Entry Account") + journals_with_reference = ( + qb.from_(jea) + .select(jea.parent) + .distinct() + .where(jea.reference_type.isin(advance_doctypes) & jea.docstatus.eq(1)) + .run() + ) + if journals_with_reference: + je = qb.DocType("Journal Entry") + advance_journal_entries = ( + qb.from_(je) + .select(ConstantColumn("Journal Entry").as_("doctype")) + .select(je.name) + .where(je.name.isin(journals_with_reference) & je.docstatus.eq(1)) + .run(as_dict=True) + ) + + return advance_journal_entries + + +def make_advance_ledger_entries(vouchers: list): + for x in vouchers: + frappe.get_doc(x.doctype, x.name).make_advance_payment_ledger_entries() + + +def execute(): + """ + Description: + Create Advance Payment Ledger Entry for all Payments made against Sales / Purchase Orders + """ + frappe.db.truncate("Advance Payment Ledger Entry") + payment_entries = get_payments_with_so_po_reference() + make_advance_ledger_entries(payment_entries) + + journals = get_journals_with_so_po_reference() + make_advance_ledger_entries(journals) diff --git a/erpnext/patches/v15_0/drop_index_posting_datetime_from_sle.py b/erpnext/patches/v15_0/drop_index_posting_datetime_from_sle.py new file mode 100644 index 000000000000..40b0b2d67595 --- /dev/null +++ b/erpnext/patches/v15_0/drop_index_posting_datetime_from_sle.py @@ -0,0 +1,16 @@ +import click +import frappe + + +def execute(): + table = "tabStock Ledger Entry" + index = "posting_datetime_creation_index" + + if not frappe.db.has_index(table, index): + return + + try: + frappe.db.sql_ddl(f"ALTER TABLE `{table}` DROP INDEX `{index}`") + click.echo(f"✓ dropped {index} index from {table}") + except Exception: + frappe.log_error("Failed to drop index") diff --git a/erpnext/patches/v15_0/enable_allow_existing_serial_no.py b/erpnext/patches/v15_0/enable_allow_existing_serial_no.py new file mode 100644 index 000000000000..e13adc2b1875 --- /dev/null +++ b/erpnext/patches/v15_0/enable_allow_existing_serial_no.py @@ -0,0 +1,6 @@ +import frappe + + +def execute(): + if frappe.get_all("Company", filters={"country": "India"}, limit=1): + frappe.db.set_single_value("Stock Settings", "allow_existing_serial_no", 1) diff --git a/erpnext/patches/v15_0/link_purchase_item_to_asset_doc.py b/erpnext/patches/v15_0/link_purchase_item_to_asset_doc.py new file mode 100644 index 000000000000..662858e52a45 --- /dev/null +++ b/erpnext/patches/v15_0/link_purchase_item_to_asset_doc.py @@ -0,0 +1,74 @@ +import frappe + + +def execute(): + if frappe.db.has_column("Asset", "purchase_invoice_item") and frappe.db.has_column( + "Asset", "purchase_receipt_item" + ): + # Get all assets with their related Purchase Invoice and Purchase Receipt + assets = frappe.get_all( + "Asset", + filters={"docstatus": 0}, + fields=[ + "name", + "item_code", + "purchase_invoice", + "purchase_receipt", + "gross_purchase_amount", + "asset_quantity", + "purchase_invoice_item", + "purchase_receipt_item", + ], + ) + + for asset in assets: + # Get Purchase Invoice Items + if asset.purchase_invoice and not asset.purchase_invoice_item: + purchase_invoice_item = get_linked_item( + "Purchase Invoice Item", + asset.purchase_invoice, + asset.item_code, + asset.gross_purchase_amount, + asset.asset_quantity, + ) + frappe.db.set_value("Asset", asset.name, "purchase_invoice_item", purchase_invoice_item) + + # Get Purchase Receipt Items + if asset.purchase_receipt and not asset.purchase_receipt_item: + purchase_receipt_item = get_linked_item( + "Purchase Receipt Item", + asset.purchase_receipt, + asset.item_code, + asset.gross_purchase_amount, + asset.asset_quantity, + ) + frappe.db.set_value("Asset", asset.name, "purchase_receipt_item", purchase_receipt_item) + + +def get_linked_item(doctype, parent, item_code, amount, quantity): + items = frappe.get_all( + doctype, + filters={ + "parenttype": doctype.replace(" Item", ""), + "parent": parent, + "item_code": item_code, + }, + fields=["name", "rate", "amount", "qty", "landed_cost_voucher_amount"], + ) + if len(items) == 1: + # If only one item exists, return it directly + return items[0].name + + for item in items: + landed_cost = item.get("landed_cost_voucher_amount", 0) + # Check if the asset is grouped + if quantity > 1: + if item.amount + landed_cost == amount and item.qty == quantity: + return item.name + elif item.qty == quantity: + return item.name + else: + if item.rate + (landed_cost / item.qty) == amount: + return item.name + + return items[0].name if items else None diff --git a/erpnext/patches/v15_0/set_is_exchange_gain_loss_in_payment_entry_deductions.py b/erpnext/patches/v15_0/set_is_exchange_gain_loss_in_payment_entry_deductions.py new file mode 100644 index 000000000000..9ffe272fd5e0 --- /dev/null +++ b/erpnext/patches/v15_0/set_is_exchange_gain_loss_in_payment_entry_deductions.py @@ -0,0 +1,22 @@ +import frappe + + +def execute(): + default_exchange_gain_loss_accounts = frappe.get_all( + "Company", + filters={"exchange_gain_loss_account": ["!=", ""]}, + pluck="exchange_gain_loss_account", + ) + + if not default_exchange_gain_loss_accounts: + return + + payment_entry = frappe.qb.DocType("Payment Entry") + payment_entry_deduction = frappe.qb.DocType("Payment Entry Deduction") + + frappe.qb.update(payment_entry_deduction).set(payment_entry_deduction.is_exchange_gain_loss, 1).join( + payment_entry, + ).on(payment_entry.name == payment_entry_deduction.parent).where( + (payment_entry.paid_to_account_currency != payment_entry.paid_from_account_currency) + & (payment_entry_deduction.account.isin(default_exchange_gain_loss_accounts)) + ).run() diff --git a/erpnext/patches/v15_0/set_standard_stock_entry_type.py b/erpnext/patches/v15_0/set_standard_stock_entry_type.py new file mode 100644 index 000000000000..4a721abae402 --- /dev/null +++ b/erpnext/patches/v15_0/set_standard_stock_entry_type.py @@ -0,0 +1,17 @@ +import frappe + + +def execute(): + for stock_entry_type in [ + "Material Issue", + "Material Receipt", + "Material Transfer", + "Material Transfer for Manufacture", + "Material Consumption for Manufacture", + "Manufacture", + "Repack", + "Send to Subcontractor", + "Disassemble", + ]: + if frappe.db.exists("Stock Entry Type", stock_entry_type): + frappe.db.set_value("Stock Entry Type", stock_entry_type, "is_standard", 1) diff --git a/erpnext/patches/v15_0/update_invoice_remarks.py b/erpnext/patches/v15_0/update_invoice_remarks.py new file mode 100644 index 000000000000..9146713815f5 --- /dev/null +++ b/erpnext/patches/v15_0/update_invoice_remarks.py @@ -0,0 +1,149 @@ +import frappe +from frappe import _ + + +def execute(): + update_sales_invoice_remarks() + update_purchase_invoice_remarks() + + +def update_sales_invoice_remarks(): + """ + Update remarks in Sales Invoice. + Some sites may have very large volume of sales invoices. + In such cases, updating documents one by one won't be successful, especially during site migration step. + Refer to the bug report: https://github.com/frappe/erpnext/issues/43634 + In this case, a bulk update must be done. + + Step 1: Update remarks in GL Entries + Step 2: Update remarks in Payment Ledger Entries + Step 3: Update remarks in Sales Invoice - Should be last step + """ + + ### Step 1: Update remarks in GL Entries + update_sales_invoice_gle_remarks() + + ### Step 2: Update remarks in Payment Ledger Entries + update_sales_invoice_ple_remarks() + + ### Step 3: Update remarks in Sales Invoice + update_query = """ + UPDATE `tabSales Invoice` + SET remarks = concat('Against Customer Order ', po_no) + WHERE po_no <> '' AND docstatus = %(docstatus)s and remarks = %(remarks)s + """ + + # Data for update query + values = {"remarks": "No Remarks", "docstatus": 1} + + # Execute query + frappe.db.sql(update_query, values=values, as_dict=0) + + +def update_purchase_invoice_remarks(): + """ + Update remarks in Purchase Invoice. + Some sites may have very large volume of purchase invoices. + In such cases, updating documents one by one wont be successful, especially during site migration step. + Refer to the bug report: https://github.com/frappe/erpnext/issues/43634 + In this case, a bulk update must be done. + + Step 1: Update remarks in GL Entries + Step 2: Update remarks in Payment Ledger Entries + Step 3: Update remarks in Purchase Invoice - Should be last step + """ + + ### Step 1: Update remarks in GL Entries + update_purchase_invoice_gle_remarks() + + ### Step 2: Update remarks in Payment Ledger Entries + update_purchase_invoice_ple_remarks() + + ### Step 3: Update remarks in Purchase Invoice + update_query = """ + UPDATE `tabPurchase Invoice` + SET remarks = concat('Against Supplier Invoice ', bill_no) + WHERE bill_no <> '' AND docstatus = %(docstatus)s and remarks = %(remarks)s + """ + + # Data for update query + values = {"remarks": "No Remarks", "docstatus": 1} + + # Execute query + frappe.db.sql(update_query, values=values, as_dict=0) + + +def update_sales_invoice_gle_remarks(): + ## Update query to update GL Entry - Updates all entries which are for Sales Invoice with No Remarks + update_query = """ + UPDATE + `tabGL Entry` as gle + INNER JOIN `tabSales Invoice` as si + ON gle.voucher_type = 'Sales Invoice' AND gle.voucher_no = si.name AND gle.remarks = %(remarks)s + SET + gle.remarks = concat('Against Customer Order ', si.po_no) + WHERE si.po_no <> '' AND si.docstatus = %(docstatus)s and si.remarks = %(remarks)s + """ + + # Data for update query + values = {"remarks": "No Remarks", "docstatus": 1} + + # Execute query + frappe.db.sql(update_query, values=values, as_dict=0) + + +def update_sales_invoice_ple_remarks(): + ## Update query to update Payment Ledger Entry - Updates all entries which are for Sales Invoice with No Remarks + update_query = """ + UPDATE + `tabPayment Ledger Entry` as ple + INNER JOIN `tabSales Invoice` as si + ON ple.voucher_type = 'Sales Invoice' AND ple.voucher_no = si.name AND ple.remarks = %(remarks)s + SET + ple.remarks = concat('Against Customer Order ', si.po_no) + WHERE si.po_no <> '' AND si.docstatus = %(docstatus)s and si.remarks = %(remarks)s + """ + + ### Data for update query + values = {"remarks": "No Remarks", "docstatus": 1} + + ### Execute query + frappe.db.sql(update_query, values=values, as_dict=0) + + +def update_purchase_invoice_gle_remarks(): + ### Query to update GL Entry - Updates all entries which are for Purchase Invoice with No Remarks + update_query = """ + UPDATE + `tabGL Entry` as gle + INNER JOIN `tabPurchase Invoice` as pi + ON gle.voucher_type = 'Purchase Invoice' AND gle.voucher_no = pi.name AND gle.remarks = %(remarks)s + SET + gle.remarks = concat('Against Supplier Invoice ', pi.bill_no) + WHERE pi.bill_no <> '' AND pi.docstatus = %(docstatus)s and pi.remarks = %(remarks)s + """ + + ### Data for update query + values = {"remarks": "No Remarks", "docstatus": 1} + + ### Execute query + frappe.db.sql(update_query, values=values, as_dict=0) + + +def update_purchase_invoice_ple_remarks(): + ### Query to update Payment Ledger Entry - Updates all entries which are for Purchase Invoice with No Remarks + update_query = """ + UPDATE + `tabPayment Ledger Entry` as ple + INNER JOIN `tabPurchase Invoice` as pi + ON ple.voucher_type = 'Purchase Invoice' AND ple.voucher_no = pi.name AND ple.remarks = %(remarks)s + SET + ple.remarks = concat('Against Supplier Invoice ', pi.bill_no) + WHERE pi.bill_no <> '' AND pi.docstatus = %(docstatus)s and pi.remarks = %(remarks)s + """ + + ### Data for update query + values = {"remarks": "No Remarks", "docstatus": 1} + + ### Execute query + frappe.db.sql(update_query, values=values, as_dict=0) diff --git a/erpnext/patches/v15_0/update_sub_voucher_type_in_gl_entries.py b/erpnext/patches/v15_0/update_sub_voucher_type_in_gl_entries.py new file mode 100644 index 000000000000..7160a6ba87d9 --- /dev/null +++ b/erpnext/patches/v15_0/update_sub_voucher_type_in_gl_entries.py @@ -0,0 +1,57 @@ +import frappe + + +def execute(): + update_purchase_invoices() + update_sales_invoices() + update_sales_debit_notes() + + +def update_purchase_invoices(): + invoices = frappe.get_all( + "Purchase Invoice", + filters={"docstatus": 1, "is_return": 0}, + pluck="name", + ) + + if not invoices: + return + + update_gl_entry(doctype="Purchase Invoice", invoices=invoices, value="Purchase Invoice") + + +def update_sales_invoices(): + invoices = frappe.get_all( + "Sales Invoice", + filters={"docstatus": 1, "is_return": 0, "is_debit_note": 0}, + pluck="name", + ) + if not invoices: + return + + update_gl_entry(doctype="Sales Invoice", invoices=invoices, value="Sales Invoice") + + +def update_sales_debit_notes(): + invoices = frappe.get_all( + "Sales Invoice", + filters={"docstatus": 1, "is_debit_note": 1}, + pluck="name", + ) + + if not invoices: + return + + update_gl_entry(doctype="Sales Invoice", invoices=invoices, value="Debit Note") + + +def update_gl_entry(doctype, invoices, value): + gl_entry = frappe.qb.DocType("GL Entry") + ( + frappe.qb.update(gl_entry) + .set("voucher_subtype", value) + .where(gl_entry.voucher_subtype.isnotnull()) + .where(gl_entry.voucher_no.isin(invoices)) + .where(gl_entry.voucher_type == doctype) + .run() + ) diff --git a/erpnext/patches/v15_0/update_task_assignee_email_field_in_asset_maintenance_log.py b/erpnext/patches/v15_0/update_task_assignee_email_field_in_asset_maintenance_log.py new file mode 100644 index 000000000000..a6eda9c2e17c --- /dev/null +++ b/erpnext/patches/v15_0/update_task_assignee_email_field_in_asset_maintenance_log.py @@ -0,0 +1,18 @@ +import frappe +from frappe.query_builder import DocType + + +def execute(): + if frappe.db.has_column("Asset Maintenance Log", "task_assignee_email"): + asset_maintenance_log = DocType("Asset Maintenance Log") + asset_maintenance_task = DocType("Asset Maintenance Task") + try: + ( + frappe.qb.update(asset_maintenance_log) + .set(asset_maintenance_log.task_assignee_email, asset_maintenance_task.assign_to) + .join(asset_maintenance_task) + .on(asset_maintenance_log.task == asset_maintenance_task.name) + .run() + ) + except Exception: + frappe.log_error("Failed to update Task Assignee Email Field.") diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index d03ab786cc11..643e3b21782e 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -45,6 +45,7 @@ frappe.ui.form.on("Project", { frm.set_query("sales_order", function () { var filters = { project: ["in", frm.doc.__islocal ? [""] : [frm.doc.name, ""]], + company: frm.doc.company, }; if (frm.doc.customer) { diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index b09735e56443..f1512501e76f 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -93,14 +93,14 @@ def before_print(self, settings=None): def validate(self): if not self.is_new(): - self.copy_from_template() + self.copy_from_template() # nosemgrep self.send_welcome_email() self.update_costing() self.update_percent_complete() self.validate_from_to_dates("expected_start_date", "expected_end_date") self.validate_from_to_dates("actual_start_date", "actual_end_date") - def copy_from_template(self): + def copy_from_template(self): # nosemgrep """ Copy tasks from template """ @@ -205,7 +205,7 @@ def update_project(self): self.db_update() def after_insert(self): - self.copy_from_template() + self.copy_from_template() # nosemgrep if self.sales_order: frappe.db.set_value("Sales Order", self.sales_order, "project", self.name) @@ -213,6 +213,13 @@ def on_trash(self): frappe.db.set_value("Sales Order", {"project": self.name}, "project", "") def update_percent_complete(self): + if self.status == "Completed": + if ( + len(frappe.get_all("Task", dict(project=self.name))) == 0 + ): # A project without tasks should be able to complete + self.percent_complete_method = "Manual" + self.percent_complete = 100 + if self.percent_complete_method == "Manual": if self.status == "Completed": self.percent_complete = 100 @@ -317,9 +324,13 @@ def update_sales_amount(self): self.total_sales_amount = total_sales_amount and total_sales_amount[0][0] or 0 def update_billed_amount(self): + # nosemgrep total_billed_amount = frappe.db.sql( - """select sum(base_net_total) - from `tabSales Invoice` where project = %s and docstatus=1""", + """select sum(base_net_amount) + from `tabSales Invoice Item` si_item, `tabSales Invoice` si + where si_item.parent = si.name + and if(si_item.project, si_item.project, si.project) = %s + and si.docstatus=1""", self.name, ) @@ -669,31 +680,8 @@ def update_project_sales_billing(): return # Else simply fallback to Daily - exists_query = "(SELECT 1 from `tab{doctype}` where docstatus = 1 and project = `tabProject`.name)" - project_map = {} - for project_details in frappe.db.sql( - """ - SELECT name, 1 as order_exists, null as invoice_exists from `tabProject` where - exists {order_exists} - union - SELECT name, null as order_exists, 1 as invoice_exists from `tabProject` where - exists {invoice_exists} - """.format( - order_exists=exists_query.format(doctype="Sales Order"), - invoice_exists=exists_query.format(doctype="Sales Invoice"), - ), - as_dict=True, - ): - project = project_map.setdefault( - project_details.name, frappe.get_doc("Project", project_details.name) - ) - if project_details.order_exists: - project.update_sales_amount() - if project_details.invoice_exists: - project.update_billed_amount() - - for project in project_map.values(): - project.save() + for project in frappe.get_all("Project", filters={"status": ["!=", "Cancelled"]}): + frappe.get_doc("Project", project.name).save() @frappe.whitelist() @@ -744,7 +732,6 @@ def get_users_email(doc): def calculate_total_purchase_cost(project: str | None = None): if project: pitem = qb.DocType("Purchase Invoice Item") - frappe.qb.DocType("Purchase Invoice Item") total_purchase_cost = ( qb.from_(pitem) .select(Sum(pitem.base_net_amount)) diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index 1b7460f7a2a0..e5996c2da9d2 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -199,6 +199,34 @@ def test_project_with_template_tasks_having_common_name(self): if not pt.is_group: self.assertIsNotNone(pt.parent_task) + def test_project_having_no_tasks_complete(self): + project_name = "Test Project - No Tasks Completion" + frappe.db.sql(""" delete from tabTask where project = %s """, project_name) + frappe.delete_doc("Project", project_name) + + project = frappe.get_doc( + { + "doctype": "Project", + "project_name": project_name, + "status": "Open", + "expected_start_date": nowdate(), + "company": "_Test Company", + } + ).insert() + + tasks = frappe.get_all( + "Task", + ["subject", "exp_end_date", "depends_on_tasks", "name", "parent_task"], + dict(project=project.name), + order_by="creation asc", + ) + + self.assertEqual(project.status, "Open") + self.assertEqual(len(tasks), 0) + project.status = "Completed" + project.save() + self.assertEqual(project.status, "Completed") + def get_project(name, template): project = frappe.get_doc( diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index c03c99b25d51..5eae55d71c59 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -153,14 +153,14 @@ def validate_dependencies_for_template_task(self): def validate_parent_template_task(self): if self.parent_task: if not frappe.db.get_value("Task", self.parent_task, "is_template"): - parent_task_format = f"""{self.parent_task}""" + parent_task_format = f"""{self.parent_task}""" frappe.throw(_("Parent Task {0} is not a Template Task").format(parent_task_format)) def validate_depends_on_tasks(self): if self.depends_on: for task in self.depends_on: if not frappe.db.get_value("Task", task.task, "is_template"): - dependent_task_format = f"""{task.task}""" + dependent_task_format = f"""{task.task}""" frappe.throw(_("Dependent Task {0} is not a Template Task").format(dependent_task_format)) def validate_completed_on(self): diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js index 9aba75b3ce92..168b891e98c3 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.js +++ b/erpnext/projects/doctype/timesheet/timesheet.js @@ -58,10 +58,10 @@ frappe.ui.form.on("Timesheet", { } if (frm.doc.docstatus < 1) { - let button = "Start Timer"; + let button = __("Start Timer"); $.each(frm.doc.time_logs || [], function (i, row) { if (row.from_time <= frappe.datetime.now_datetime() && !row.completed) { - button = "Resume Timer"; + button = __("Resume Timer"); } }); diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index fb15b507efbf..7ab661c8822c 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -22,6 +22,46 @@ class OverWorkLoggedError(frappe.ValidationError): class Timesheet(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from erpnext.projects.doctype.timesheet_detail.timesheet_detail import TimesheetDetail + + amended_from: DF.Link | None + base_total_billable_amount: DF.Currency + base_total_billed_amount: DF.Currency + base_total_costing_amount: DF.Currency + company: DF.Link | None + currency: DF.Link | None + customer: DF.Link | None + department: DF.Link | None + employee: DF.Link | None + employee_name: DF.Data | None + end_date: DF.Date | None + exchange_rate: DF.Float + naming_series: DF.Literal["TS-.YYYY.-"] + note: DF.TextEditor | None + parent_project: DF.Link | None + per_billed: DF.Percent + sales_invoice: DF.Link | None + start_date: DF.Date | None + status: DF.Literal["Draft", "Submitted", "Billed", "Payslip", "Completed", "Cancelled"] + time_logs: DF.Table[TimesheetDetail] + title: DF.Data | None + total_billable_amount: DF.Currency + total_billable_hours: DF.Float + total_billed_amount: DF.Currency + total_billed_hours: DF.Float + total_costing_amount: DF.Currency + total_hours: DF.Float + user: DF.Link | None + # end: auto-generated types + def validate(self): self.set_status() self.validate_dates() @@ -129,10 +169,14 @@ def update_task_and_project(self): task.save() tasks.append(data.task) - elif data.project and data.project not in projects: - frappe.get_doc("Project", data.project).update_project() + if data.project and data.project not in projects: projects.append(data.project) + for project in projects: + project_doc = frappe.get_doc("Project", project) + project_doc.update_project() + project_doc.save() + def validate_dates(self): for data in self.time_logs: if data.from_time and data.to_time and time_diff_in_hours(data.to_time, data.from_time) < 0: diff --git a/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py index 766e40e319c5..dc3da2596622 100644 --- a/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py +++ b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py @@ -77,7 +77,7 @@ def get_chart_data(data): charts = { "data": { "labels": [_("On Track"), _("Delayed")], - "datasets": [{"name": "Delayed", "values": [on_track, delay]}], + "datasets": [{"name": _("Delayed"), "values": [on_track, delay]}], }, "type": "percentage", "colors": ["#84D5BA", "#CB4B5F"], diff --git a/erpnext/projects/report/project_summary/project_summary.py b/erpnext/projects/report/project_summary/project_summary.py index 7a35fd236a01..261517f02c6b 100644 --- a/erpnext/projects/report/project_summary/project_summary.py +++ b/erpnext/projects/report/project_summary/project_summary.py @@ -15,6 +15,7 @@ def execute(filters=None): filters=filters, fields=[ "name", + "project_name", "status", "percent_complete", "expected_start_date", @@ -48,6 +49,11 @@ def get_columns(): "options": "Project", "width": 200, }, + { + "fieldname": "project_name", + "label": _("Project Name"), + "width": 200, + }, { "fieldname": "project_type", "label": _("Type"), @@ -82,7 +88,7 @@ def get_chart_data(data): overdue = [] for project in data: - labels.append(project.name) + labels.append(project.project_name) total.append(project.total_tasks) completed.append(project.completed_tasks) overdue.append(project.overdue_tasks) diff --git a/erpnext/public/images/erpnext-logo-blue.png b/erpnext/public/images/erpnext-logo-blue.png new file mode 100644 index 0000000000000000000000000000000000000000..0134b3f0627c5bc95aee1682f7979f9590751b4e GIT binary patch literal 4235 zcmds5X;f3!7Cs50f>0}zp$e$27MUc15RF0{7=$nd1B5|>BA^i&gvcO}I8Z<)6&lb8 zC}T*%Ok^f;K-4f<3?LW~kXV8cQw&I$Ug%q^|K5+jAMIQ3$60sXefC}F+xy#hf9ISe zCkJaq`K|H*07aV<7R~@bxNBqMda0$P;q<)pAs2GOJq!SS)Y^a)nUPDSMo5^m^)XQ1 zr75B{wM%d>6?~L%L4H24jYT3m`KPpuk(_ZUo>0vu~xS9s%57AWw}c()pk4k zYSb6xVQ!>Y*?~x~S$Wl162aQozeB zGStK zj_lvbn=%L38wLE(0sZfZ%G=!%s-E{5(j2F925xSq>`uILdaEqfbXjLE`D-EKrb&$! z+5puQ4Bt{V9;>XwezoUnY_$XG8C{dRg7Ij--dhs}!CJaBpBs~$<&HPrLmRZ)PDKXl z%3_WE&ez*j=tX}bF!Obb0p+yic;i;I0jIfc47E>o0JghaJCx``j?X$oYf;>)1s7_q z1C$GvX$^aWF?rnWCf$n;N1*SSuD$W;bnU7_vLUToP<`rs7^($$6?om8eQf0YvC#{? z%!t#-;Ro*oFgrc-bfgLD397bq^^yhie&U;(6$N#rW*~*eQ}B%-PhcP$YqL5=hl``e|@OIbut`3dqBNOr1G zZLHb`F%L^xuo6GmTX%M`k68jboX&UjCm-lSu&7$$s}tVqK*#Y0i?FWKIog7?ukn}RHHEUDXo;e z8+TOJC+gF4PtjrjbEX+IUnLubuw>4~ z-^oNytkcwtgAJ(EooOrKy4~I-Es2&GR%*4`cYu*LUVI~>Iw;aS7IP(nqN6Nu0BnKH z&<&!*m)0^=H@yxpigW5!a8}nQ^9K*@dl_T{7>^KhMh;&0VixK&tPScy!rH$FeU^0h z^(*y=6ghkI(ns%}D3VNi&y%lm8jdO7)0a}Ibr}l!I1bI4%7!xOtfir*7=8jE5llvn zK7Y-?3!|lgamssT($%;D;+D!!o7j?}BwQ+y!1;~Cx@-^i=?x#7J3u*>VqQ92{Za;O z^Y5_VX(grT{yKK?*73rk*FSMR_lfr^gUUOC*V_)nIGYqfD<`e)yCyi&iZQ^Hm)I-l z%%JJ0fM~01*Wg@?eJSWMeo`B~T!^=oB_;oCNALf`v;N$R+QG(A_>3$FDXta$Y<6{n zHCKxTU;~O*pg4GGoH}6d*nVpMF_%hK&eUIqvI&pIYoddDHnh=V& z?cEwGVoD7>51r6|-`5b)1OX}KVO2Q+Xy%tqV7%c?*K9k+o(5oP(LIHd=Hb>Q2!Or0 ze3*`#uTkS7H|YYq5ASSR%X&Yy0Px(ct!cS#*_S3<4`4|s4(pdPIf%~>V)x^oCAbhf z74TCHk9S1J7DM!0`*1v5rpPtWT_Q`;na(V6t>*L)Rdw<2{@w5&f%n%^6e!#AIC;ccW5tdAq?Rb3eGu)p_tob8$TuWH zz=GO8mgBz`#D7%?YcvilHt|LC5aX~f?Q1ORud%qBnoiV&85_nK()4pDaDRJw{q@k_ z%KPXWveLiuH$T2}e)YS`kbHSx32*z4-b>cN>-7IOi+^V85mcQsj+@tF zjD4N|`CjoSy#Lg%)~fKp3d&-!gcOfh$s^sRjXfxr0oe}PL7prB!}tp=n3{kcuH|wu zx49Mu#6( z@hj-EF&H3;axR=Rq-jc}0(QI4{W!fJ7hbIdQiNZ26^$*O!h6dCSR*RI(X&4m00lHo z<3xA9#Hv|}EW9jP<~(8aX@EGwo)94e)cN%Xb0zSPte(ctBAXaxzz*SZRfhYDaNDJd z=M_u%wnd$5&3)+k7{cBd*MLZTR339uyHy^%k9KKJTo%0gHrj;7tRX?(TZ!L?-J)F` zSz@d_T2Q1P{@VOA{G6Q+a1pC|z@OuHI9){FP$;hHz|f}@k*0`JSH{nT?%iAFr7U=2zLypY?LPIN`#V%;G@;SBWKYD=A~(kS#JRwu_wV)+iPxm+v=wgPT=W{ zY{9lDy(bhq^U}GN*QeG?Hw%p7;R$28;&lA#R~nBDu$B`}{O{V)-4t_L{5V}Ytt~R- za!%Y)=H_d>o35uQ0=;pYFbx7y$)1@1tKX_Cad_z@QXXKvaVAGw)L#Dn6StLEu4S?C z6w1DYxQQD(+Hqv1a4^D&buJgF0I*uSsS#&b_g=naE@a?GD4iov8h`UY?c9}nn(Ufp)bRgJ}2 z#Lh`Xaynkb7{0i2Jw}s!9indSKC{fK&xn`gB}dYI8TwsbJa z5apCSk&Wp-L=~ELcRzh2&)$JD&B?8cK~IMDoYa1C7g~!Y zr>xID^W~U_DLzsK39mp!r=I^=oF<5BS&pA?R1SPruA}N>)m8l3t#fJ=;ZWHB_CD3S zFFjB$P(_HyQ1>}AJ(FZbSlWxQw_E-7a9`xsmX~}h!Y!r24F78v3EFR~DZPg}-Rhcz z^cni3>VUqVfI+bimgx7Vtqe2==d~gQ!`mtS76hf1np8+%7S`dzDul`$3P%<1^_1Qn O02@mOi}GVW*Zu`KlJAZH literal 0 HcmV?d00001 diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js index 6ff38f91082f..bab25b6eca2c 100644 --- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js +++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js @@ -374,6 +374,14 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { label: "Cost Center", options: "Cost Center", depends_on: "eval:doc.action=='Create Voucher' && doc.document_type=='Payment Entry'", + get_query: () => { + return { + filters: { + is_group: 0, + company: this.company, + }, + }; + }, }, { fieldtype: "Section Break", diff --git a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js index 5061be9d20a2..21aa70fe7ae5 100644 --- a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js +++ b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js @@ -25,6 +25,9 @@ class BOMConfigurator { }; frappe.views.trees["BOM Configurator"] = new frappe.views.TreeView(options); + let node = frappe.views.trees["BOM Configurator"].tree.root_node; + frappe.views.trees["BOM Configurator"].tree.show_toolbar(node); + frappe.views.trees["BOM Configurator"].tree.load_children(node, true); this.tree_view = frappe.views.trees["BOM Configurator"]; } @@ -137,7 +140,7 @@ class BOMConfigurator { btnClass: "hidden-xs", }, { - label: __("Expand All"), + label: __("Collapse All"), click: function (node) { let view = frappe.views.trees["BOM Configurator"]; @@ -283,6 +286,13 @@ class BOMConfigurator { fieldtype: "Float", reqd: 1, read_only: read_only, + change() { + this.layout.fields_dict.items.grid.data.forEach((row) => { + row.qty = flt(this.value); + }); + + this.layout.fields_dict.items.grid.refresh(); + }, }, { fieldtype: "Section Break" }, { diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index c39fb5242645..c7b08f1dc156 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -160,7 +160,7 @@ erpnext.accounts.taxes = { let tax = frappe.get_doc(cdt, cdn); try { me.validate_taxes_and_charges(cdt, cdn); - me.validate_inclusive_tax(tax); + me.validate_inclusive_tax(tax, frm); } catch(e) { tax.included_in_print_rate = 0; refresh_field("included_in_print_rate", tax.name, tax.parentfield); @@ -170,7 +170,8 @@ erpnext.accounts.taxes = { }); }, - validate_inclusive_tax: function(tax) { + validate_inclusive_tax: function(tax, frm) { + this.frm = this.frm || frm; let actual_type_error = function() { var msg = __("Actual type tax cannot be included in Item rate in row {0}", [tax.idx]) frappe.throw(msg); @@ -186,12 +187,12 @@ erpnext.accounts.taxes = { if(tax.charge_type == "Actual") { // inclusive tax cannot be of type Actual actual_type_error(); - } else if(tax.charge_type == "On Previous Row Amount" && + } else if(tax.charge_type == "On Previous Row Amount" && this.frm && !cint(this.frm.doc["taxes"][tax.row_id - 1].included_in_print_rate) ) { // referred row should also be an inclusive tax on_previous_row_error(tax.row_id); - } else if(tax.charge_type == "On Previous Row Total") { + } else if(tax.charge_type == "On Previous Row Total" && this.frm) { var taxes_not_included = $.map(this.frm.doc["taxes"].slice(0, tax.row_id), function(t) { return cint(t.included_in_print_rate) ? null : t; }); if(taxes_not_included.length > 0) { diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index b5a8b7577060..202efe157f08 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -342,12 +342,15 @@ erpnext.buying = { add_serial_batch_bundle(doc, cdt, cdn) { let item = locals[cdt][cdn]; let me = this; + let fields = ["has_batch_no", "has_serial_no"]; - frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]) + frappe.db.get_value("Item", item.item_code, fields) .then((r) => { if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) { - item.has_serial_no = r.message.has_serial_no; - item.has_batch_no = r.message.has_batch_no; + fields.forEach((field) => { + item[field] = r.message[field]; + }); + item.type_of_transaction = item.qty > 0 ? "Inward" : "Outward"; item.is_rejected = false; @@ -380,13 +383,16 @@ erpnext.buying = { add_serial_batch_for_rejected_qty(doc, cdt, cdn) { let item = locals[cdt][cdn]; let me = this; + let fields = ["has_batch_no", "has_serial_no"]; - frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]) + frappe.db.get_value("Item", item.item_code, fields) .then((r) => { if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) { - item.has_serial_no = r.message.has_serial_no; - item.has_batch_no = r.message.has_batch_no; - item.type_of_transaction = item.rejected_qty > 0 ? "Inward" : "Outward"; + fields.forEach((field) => { + item[field] = r.message[field]; + }); + + item.type_of_transaction = !doc.is_return > 0 ? "Inward" : "Outward"; item.is_rejected = true; new erpnext.SerialBatchPackageSelector( @@ -398,7 +404,7 @@ erpnext.buying = { } let update_values = { - "serial_and_batch_bundle": r.name, + "rejected_serial_and_batch_bundle": r.name, "use_serial_batch_fields": 0, "rejected_qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item)) } diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index adf05ffb154d..ee78e493db66 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -103,7 +103,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { this.determine_exclusive_rate(); this.calculate_net_total(); this.calculate_taxes(); - this.manipulate_grand_total_for_inclusive_tax(); + this.adjust_grand_total_for_inclusive_tax(); this.calculate_totals(); this._cleanup(); } @@ -128,7 +128,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { calculate_item_values() { var me = this; if (!this.discount_amount_applied) { - for (const item of this.frm._items || []) { + for (const item of this.frm.doc.items || []) { frappe.model.round_floats_in(item); item.net_rate = item.rate; item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty; @@ -185,7 +185,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { if (!this.discount_amount_applied) { erpnext.accounts.taxes.validate_taxes_and_charges(tax.doctype, tax.name); - erpnext.accounts.taxes.validate_inclusive_tax(tax); + erpnext.accounts.taxes.validate_inclusive_tax(tax, this.frm); } frappe.model.round_floats_in(tax); }); @@ -227,7 +227,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { }); if(has_inclusive_tax==false) return; - $.each(me.frm._items || [], function(n, item) { + $.each(this.frm.doc.items || [], function(n, item) { var item_tax_map = me._load_item_tax_rate(item.item_tax_rate); var cumulated_tax_fraction = 0.0; var total_inclusive_tax_amount_per_qty = 0; @@ -250,7 +250,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { if(!me.discount_amount_applied && item.qty && (total_inclusive_tax_amount_per_qty || cumulated_tax_fraction)) { var amount = flt(item.amount) - total_inclusive_tax_amount_per_qty; - item.net_amount = flt(amount / (1 + cumulated_tax_fraction)); + item.net_amount = flt(amount / (1 + cumulated_tax_fraction), precision("net_amount", item)); item.net_rate = item.qty ? flt(item.net_amount / item.qty, precision("net_rate", item)) : 0; me.set_in_company_currency(item, ["net_rate", "net_amount"]); @@ -305,6 +305,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { me.frm.doc.net_total += item.net_amount; me.frm.doc.base_net_total += item.base_net_amount; }); + + frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]); } calculate_shipping_charges() { @@ -523,7 +525,15 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } } + /** + * @deprecated Use adjust_grand_total_for_inclusive_tax instead. + */ manipulate_grand_total_for_inclusive_tax() { + // for backward compatablility - if in case used by an external application + this.adjust_grand_total_for_inclusive_tax() + } + + adjust_grand_total_for_inclusive_tax() { var me = this; // if fully inclusive taxes and diff if (this.frm.doc["taxes"] && this.frm.doc["taxes"].length) { @@ -550,7 +560,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { diff = flt(diff, precision("rounding_adjustment")); if ( diff && Math.abs(diff) <= (5.0 / Math.pow(10, precision("tax_amount", last_tax))) ) { - me.frm.doc.rounding_adjustment = diff; + me.frm.doc.grand_total_diff = diff; + } else { + me.frm.doc.grand_total_diff = 0; } } } @@ -561,7 +573,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { var me = this; var tax_count = this.frm.doc["taxes"] ? this.frm.doc["taxes"].length : 0; this.frm.doc.grand_total = flt(tax_count - ? this.frm.doc["taxes"][tax_count - 1].total + flt(this.frm.doc.rounding_adjustment) + ? this.frm.doc["taxes"][tax_count - 1].total + flt(this.frm.doc.grand_total_diff) : this.frm.doc.net_total); if(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) { @@ -621,7 +633,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { if(frappe.meta.get_docfield(this.frm.doc.doctype, "rounded_total", this.frm.doc.name)) { this.frm.doc.rounded_total = round_based_on_smallest_currency_fraction(this.frm.doc.grand_total, this.frm.doc.currency, precision("rounded_total")); - this.frm.doc.rounding_adjustment += flt(this.frm.doc.rounded_total - this.frm.doc.grand_total, + this.frm.doc.rounding_adjustment = flt(this.frm.doc.rounded_total - this.frm.doc.grand_total, precision("rounding_adjustment")); this.set_in_company_currency(this.frm.doc, ["rounding_adjustment", "rounded_total"]); @@ -630,7 +642,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { _cleanup() { this.frm.doc.base_in_words = this.frm.doc.in_words = ""; - let items = this.frm._items; + let items = this.frm.doc.items; if(items && items.length) { if(!frappe.meta.get_docfield(items[0].doctype, "item_tax_amount", this.frm.doctype)) { @@ -689,8 +701,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { if (total_for_discount_amount) { $.each(this.frm._items || [], function(i, item) { distributed_amount = flt(me.frm.doc.discount_amount) * item.net_amount / total_for_discount_amount; - item.net_amount = flt(item.net_amount - distributed_amount, - precision("base_amount", item)); + item.net_amount = flt(item.net_amount - distributed_amount, precision("net_amount", item)); net_total += item.net_amount; // discount amount rounding loss adjustment if no taxes @@ -833,13 +844,13 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { ); } - if(!this.frm.doc.is_return){ - this.frm.doc.payments.find(payment => { - if (payment.default) { - payment.amount = total_amount_to_pay; - } - }); - } + this.frm.doc.payments.find(payment => { + if (payment.default) { + payment.amount = total_amount_to_pay; + } else { + payment.amount = 0 + } + }); this.frm.refresh_fields(); } diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 4696c7f3b4dc..18cddd7f7a1c 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -289,28 +289,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } ]); } - - if(this.frm.fields_dict['items'].grid.get_field('serial_and_batch_bundle')) { - let sbb_field = this.frm.get_docfield('items', 'serial_and_batch_bundle'); - if (sbb_field) { - sbb_field.get_route_options_for_new_doc = (row) => { - return { - 'item_code': row.doc.item_code, - } - }; - } - } - - if(this.frm.fields_dict['items'].grid.get_field('batch_no')) { - let batch_no_field = this.frm.get_docfield('items', 'batch_no'); - if (batch_no_field) { - batch_no_field.get_route_options_for_new_doc = function(row) { - return { - 'item': row.doc.item_code - } - }; - } - } } is_return() { @@ -375,7 +353,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe recipient_id: me.frm.doc.contact_email, payment_request_type: payment_request_type, party_type: payment_request_type == 'Outward' ? "Supplier" : "Customer", - party: payment_request_type == 'Outward' ? me.frm.doc.supplier : me.frm.doc.customer + party: payment_request_type == 'Outward' ? me.frm.doc.supplier : me.frm.doc.customer, + party_name:payment_request_type == 'Outward' ? me.frm.doc.supplier_name : me.frm.doc.customer_name }, callback: function(r) { if(!r.exc){ @@ -408,6 +387,35 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe this.setup_quality_inspection(); this.validate_has_items(); erpnext.utils.view_serial_batch_nos(this.frm); + this.set_route_options_for_new_doc(); + } + + set_route_options_for_new_doc() { + // While creating the batch from the link field, copy item from line item to batch form + + if(this.frm.fields_dict['items'].grid.get_field('batch_no')) { + let batch_no_field = this.frm.get_docfield('items', 'batch_no'); + if (batch_no_field) { + batch_no_field.get_route_options_for_new_doc = function(row) { + return { + 'item': row.doc.item_code + } + }; + } + } + + // While creating the SABB from the link field, copy item, doctype from line item to SABB form + if(this.frm.fields_dict['items'].grid.get_field('serial_and_batch_bundle')) { + let sbb_field = this.frm.get_docfield('items', 'serial_and_batch_bundle'); + if (sbb_field) { + sbb_field.get_route_options_for_new_doc = (row) => { + return { + "item_code": row.doc.item_code, + "voucher_type": this.frm.doc.doctype, + } + }; + } + } } scan_barcode() { @@ -478,7 +486,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe setup_sms() { var me = this; let blacklist = ['Purchase Invoice', 'BOM']; - if(this.frm.doc.docstatus===1 && !["Lost", "Stopped", "Closed"].includes(this.frm.doc.status) + if(frappe.boot.sms_gateway_enabled && this.frm.doc.docstatus===1 && !["Lost", "Stopped", "Closed"].includes(this.frm.doc.status) && !blacklist.includes(this.frm.doctype)) { this.frm.page.add_menu_item(__('Send SMS'), function() { me.send_sms(); }); } @@ -573,6 +581,15 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe callback: function(r) { if(!r.exc) { frappe.run_serially([ + () => { + if (item.docstatus === 0 + && frappe.meta.has_field(item.doctype, "use_serial_batch_fields") + && !item.use_serial_batch_fields + && cint(frappe.user_defaults?.use_serial_batch_fields) === 1 + ) { + item["use_serial_batch_fields"] = 1; + } + }, () => { var d = locals[cdt][cdn]; me.add_taxes_from_item_tax_template(d.item_tax_rate); @@ -939,9 +956,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") && ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'].includes(this.frm.doctype)) { - erpnext.utils.get_shipping_address(this.frm, function() { - set_party_account(set_pricing); - }); + let is_drop_ship = me.frm.doc.items.some(item => item.delivered_by_supplier); + + if (!is_drop_ship) { + console.log('get_shipping_address'); + erpnext.utils.get_shipping_address(this.frm, function() { + set_party_account(set_pricing); + }); + } } else { set_party_account(set_pricing); @@ -1102,7 +1124,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe apply_discount_on_item(doc, cdt, cdn, field) { var item = frappe.get_doc(cdt, cdn); - if(!item.price_list_rate) { + if(item && !item.price_list_rate) { item[field] = 0.0; } else { this.price_list_rate(doc, cdt, cdn); @@ -1215,8 +1237,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe }, callback: function(r) { if(!r.exc) { - me.apply_price_list(item, true) frappe.model.set_value(cdt, cdn, 'conversion_factor', r.message.conversion_factor); + me.apply_price_list(item, true); } } }); @@ -1258,6 +1280,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe "Purchase Receipt": ["purchase_order_item", "purchase_invoice_item", "purchase_receipt_item"], "Purchase Invoice": ["purchase_order_item", "pr_detail", "po_detail"], "Sales Order": ["prevdoc_docname", "quotation_item"], + "Purchase Order": ["supplier_quotation_item"], }; const mappped_fields = mapped_item_field_map[this.frm.doc.doctype] || []; @@ -1267,6 +1290,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe .filter(Boolean).length > 0; } else if (this.frm.doc?.items) { let first_row = this.frm.doc.items[0]; + if (!first_row) { + return false + }; + let mapped_rows = mappped_fields.filter(d => first_row[d]) return mapped_rows?.length > 0; @@ -1421,12 +1448,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe let show = cint(this.frm.doc.discount_amount) || ((this.frm.doc.taxes || []).filter(function(d) {return d.included_in_print_rate===1}).length); - if(frappe.meta.get_docfield(cur_frm.doctype, "net_total")) + if(this.frm.doc.doctype && frappe.meta.get_docfield(this.frm.doc.doctype, "net_total")) { this.frm.toggle_display("net_total", show); + } - if(frappe.meta.get_docfield(cur_frm.doctype, "base_net_total")) + if(this.frm.doc.doctype && frappe.meta.get_docfield(this.frm.doc.doctype, "base_net_total")) { this.frm.toggle_display("base_net_total", (show && (me.frm.doc.currency != company_currency))); - + } } change_grid_labels(company_currency) { @@ -1500,6 +1528,31 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } } + batch_no(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (row.use_serial_batch_fields && row.batch_no) { + var params = this._get_args(row); + params.batch_no = row.batch_no; + params.uom = row.uom; + + frappe.call({ + method: "erpnext.stock.get_item_details.get_batch_based_item_price", + args: { + params: params, + item_code: row.item_code, + }, + callback: function(r) { + if (!r.exc && r.message) { + row.price_list_rate = r.message; + row.rate = r.message; + refresh_field("rate", row.name, row.parentfield); + refresh_field("price_list_rate", row.name, row.parentfield); + } + } + }) + } + } + toggle_item_grid_columns(company_currency) { const me = this; // toggle columns @@ -1509,8 +1562,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe item_grid.set_column_disp(fname, me.frm.doc.currency != company_currency); }); - var show = (cint(cur_frm.doc.discount_amount)) || - ((cur_frm.doc.taxes || []).filter(function(d) {return d.included_in_print_rate===1}).length); + var show = (cint(this.frm.doc.discount_amount)) || + ((this.frm.doc.taxes || []).filter(function(d) {return d.included_in_print_rate===1}).length); $.each(["net_rate", "net_amount"], function(i, fname) { if(frappe.meta.get_docfield(item_grid.doctype, fname)) @@ -1776,7 +1829,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe if (data && data.apply_rule_on_other_items && JSON.parse(data.apply_rule_on_other_items)) { me.frm.doc.items.forEach(d => { - if (in_list(JSON.parse(data.apply_rule_on_other_items), d[data.apply_rule_on])) { + if (in_list(JSON.parse(data.apply_rule_on_other_items), d[data.apply_rule_on]) && d.item_code === data.item_code) { for(var k in data) { if (data.pricing_rule_for == "Discount Percentage" && data.apply_rule_on_other_items && k == "discount_amount") { continue; @@ -1851,8 +1904,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe callback: function(r) { if (!r.exc) { frappe.run_serially([ - () => me.frm.set_value("price_list_currency", r.message.parent.price_list_currency), - () => me.frm.set_value("plc_conversion_rate", r.message.parent.plc_conversion_rate), + () => { + if (r.message.parent.price_list_currency) + me.frm.set_value("price_list_currency", r.message.parent.price_list_currency); + }, + () => { + if (r.message.parent.plc_conversion_rate) + me.frm.set_value("plc_conversion_rate", r.message.parent.plc_conversion_rate); + }, () => { if(args.items.length) { me._set_values_for_item_list(r.message.children); @@ -1878,6 +1937,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe const fields = ["discount_percentage", "discount_amount", "margin_rate_or_amount", "rate_with_margin"]; + if (!item) { + return; + } + if(item.remove_free_item) { let items = []; @@ -2394,7 +2457,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe payment_terms_template() { var me = this; const doc = this.frm.doc; - if(doc.payment_terms_template && doc.doctype !== 'Delivery Note') { + if(doc.payment_terms_template && doc.doctype !== 'Delivery Note' && !doc.is_return) { var posting_date = doc.posting_date || doc.transaction_date; frappe.call({ method: "erpnext.controllers.accounts_controller.get_payment_terms", diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js index bd8b70af02e3..7a3877b9c460 100644 --- a/erpnext/public/js/financial_statements.js +++ b/erpnext/public/js/financial_statements.js @@ -9,25 +9,20 @@ erpnext.financial_statements = { data && column.colIndex >= 3 ) { - //Assuming that the first three columns are s.no, account name and the very first year of the accounting values, to calculate the relative percentage values of the successive columns. - const lastAnnualValue = row[column.colIndex - 1].content; - const currentAnnualvalue = data[column.fieldname]; - if (currentAnnualvalue == undefined) return "NA"; //making this not applicable for undefined/null values - let annualGrowth = 0; - if (lastAnnualValue == 0 && currentAnnualvalue > 0) { - //If the previous year value is 0 and the current value is greater than 0 - annualGrowth = 1; - } else if (lastAnnualValue > 0) { - annualGrowth = (currentAnnualvalue - lastAnnualValue) / lastAnnualValue; - } + const growthPercent = data[column.fieldname]; - const growthPercent = Math.round(annualGrowth * 10000) / 100; //calculating the rounded off percentage + if (growthPercent == undefined) return "NA"; //making this not applicable for undefined/null values - value = $(`${(growthPercent >= 0 ? "+" : "") + growthPercent + "%"}`); - if (growthPercent < 0) { - value = $(value).addClass("text-danger"); + if (column.fieldname === "total") { + value = $(`${growthPercent}`); } else { - value = $(value).addClass("text-success"); + value = $(`${(growthPercent >= 0 ? "+" : "") + growthPercent + "%"}`); + + if (growthPercent < 0) { + value = $(value).addClass("text-danger"); + } else { + value = $(value).addClass("text-success"); + } } value = $(value).wrap("

").parent().html(); @@ -38,11 +33,9 @@ erpnext.financial_statements = { this.baseData = row; } if (column.colIndex >= 2) { - //Assuming that the first two columns are s.no and account name, to calculate the relative percentage values of the successive columns. - const currentAnnualvalue = data[column.fieldname]; - const baseValue = this.baseData[column.colIndex].content; - if (currentAnnualvalue == undefined || baseValue <= 0) return "NA"; - const marginPercent = Math.round((currentAnnualvalue / baseValue) * 10000) / 100; + const marginPercent = data[column.fieldname]; + + if (marginPercent == undefined) return "NA"; //making this not applicable for undefined/null values value = $(`${marginPercent + "%"}`); if (marginPercent < 0) value = $(value).addClass("text-danger"); @@ -53,7 +46,8 @@ erpnext.financial_statements = { } if (data && column.fieldname == "account") { - value = data.account_name || value; + // first column + value = data.section_name || data.account_name || value; if (filter && filter?.text && filter?.type == "contains") { if (!value.toLowerCase().includes(filter.text)) { @@ -61,7 +55,7 @@ erpnext.financial_statements = { } } - if (data.account) { + if (data.account || data.accounts) { column.link_onclick = "erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")"; } @@ -70,7 +64,7 @@ erpnext.financial_statements = { value = default_formatter(value, row, column, data); - if (data && !data.parent_account) { + if (data && !data.parent_account && !data.parent_section) { value = $(`${value}`); var $value = $(value).css("font-weight", "bold"); @@ -84,13 +78,13 @@ erpnext.financial_statements = { return value; }, open_general_ledger: function (data) { - if (!data.account) return; + if (!data.account && !data.accounts) return; let project = $.grep(frappe.query_report.filters, function (e) { return e.df.fieldname == "project"; }); frappe.route_options = { - account: data.account, + account: data.account || data.accounts, company: frappe.query_report.get_filter_value("company"), from_date: data.from_date || data.year_start_date, to_date: data.to_date || data.year_end_date, @@ -116,14 +110,17 @@ erpnext.financial_statements = { erpnext.financial_statements.filters = get_filters(); let fiscal_year = erpnext.utils.get_fiscal_year(frappe.datetime.get_today()); + var filters = report.get_values(); - frappe.model.with_doc("Fiscal Year", fiscal_year, function (r) { - var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); - frappe.query_report.set_filter_value({ - period_start_date: fy.year_start_date, - period_end_date: fy.year_end_date, + if (!filters.period_start_date || !filters.period_end_date) { + frappe.model.with_doc("Fiscal Year", fiscal_year, function (r) { + var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); + frappe.query_report.set_filter_value({ + period_start_date: fy.year_start_date, + period_end_date: fy.year_end_date, + }); }); - }); + } if (report.page) { const views_menu = report.page.add_custom_button_group(__("Financial Statements")); diff --git a/erpnext/public/js/queries.js b/erpnext/public/js/queries.js index d7edac3cb9fb..fd5b7603844c 100644 --- a/erpnext/public/js/queries.js +++ b/erpnext/public/js/queries.js @@ -56,6 +56,17 @@ $.extend(erpnext.queries, { } }, + company_contact_query: function (doc) { + if (!doc.company) { + frappe.throw(__("Please set {0}", [__(frappe.meta.get_label(doc.doctype, "company", doc.name))])); + } + + return { + query: "frappe.contacts.doctype.contact.contact.contact_query", + filters: { link_doctype: "Company", link_name: doc.company }, + }; + }, + address_query: function (doc) { if (frappe.dynamic_link) { if (!doc[frappe.dynamic_link.fieldname]) { @@ -77,9 +88,13 @@ $.extend(erpnext.queries, { }, company_address_query: function (doc) { + if (!doc.company) { + frappe.throw(__("Please set {0}", [__(frappe.meta.get_label(doc.doctype, "company", doc.name))])); + } + return { query: "frappe.contacts.doctype.address.address.address_query", - filters: { is_your_company_address: 1, link_doctype: "Company", link_name: doc.company || "" }, + filters: { link_doctype: "Company", link_name: doc.company }, }; }, diff --git a/erpnext/public/js/templates/visual_plant_floor_template.html b/erpnext/public/js/templates/visual_plant_floor_template.html index 2e67085c0221..a1639f073702 100644 --- a/erpnext/public/js/templates/visual_plant_floor_template.html +++ b/erpnext/public/js/templates/visual_plant_floor_template.html @@ -1,5 +1,16 @@ {% $.each(workstations, (idx, row) => { %}
+
+ {% if(row.status == "Production") { %} +
+
+
+
+ {% } %} + + {{row.status}} + +
-
-

{{row.status}}

-
{{row.workstation_name}}
+
+ + {{row.workstation_name}} +
-{% }); %} \ No newline at end of file +{% }); %} diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index a7d88edcafab..7c02fefc0f96 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -920,6 +920,7 @@ erpnext.utils.map_current_doc = function (opts) { target: opts.target, date_field: opts.date_field || undefined, setters: opts.setters, + read_only_setters: opts.read_only_setters, data_fields: data_fields, get_query: opts.get_query, add_filters_group: 1, diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js index 91a85aa02a54..ca2bed20c7f1 100644 --- a/erpnext/public/js/utils/sales_common.js +++ b/erpnext/public/js/utils/sales_common.js @@ -15,10 +15,11 @@ erpnext.sales_common = { onload() { super.onload(); this.setup_queries(); - this.frm.set_query("shipping_rule", function () { + this.frm.set_query("shipping_rule", function (doc) { return { filters: { shipping_rule_type: "Selling", + company: doc.company, }, }; }); @@ -28,6 +29,7 @@ erpnext.sales_common = { query: "erpnext.controllers.queries.get_project_name", filters: { customer: doc.customer, + company: doc.company, }, }; }); @@ -47,9 +49,11 @@ erpnext.sales_common = { ); me.frm.set_query("contact_person", erpnext.queries.contact_query); + me.frm.set_query("company_contact_person", erpnext.queries.company_contact_query); me.frm.set_query("customer_address", erpnext.queries.address_query); me.frm.set_query("shipping_address_name", erpnext.queries.address_query); me.frm.set_query("dispatch_address_name", erpnext.queries.dispatch_address_query); + me.frm.set_query("company_address", erpnext.queries.company_address_query); erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype); diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 78efb46f4c34..de1faf36ef58 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -16,7 +16,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { let label = this.item?.has_serial_no ? __("Serial Nos") : __("Batch Nos"); let primary_label = this.bundle ? __("Update") : __("Add"); - if (this.item?.has_serial_no && this.item?.batch_no) { + if (this.item?.has_serial_no && this.item?.has_batch_no) { label = __("Serial Nos / Batch Nos"); } @@ -24,6 +24,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { this.dialog = new frappe.ui.Dialog({ title: this.item?.title || primary_label, + size: "large", fields: this.get_dialog_fields(), primary_action_label: primary_label, primary_action: () => this.update_bundle_entries(), @@ -95,7 +96,12 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { options: "Warehouse", default: this.get_warehouse(), onchange: () => { - this.item.warehouse = this.dialog.get_value("warehouse"); + if (this.item?.is_rejected) { + this.item.rejected_warehouse = this.dialog.get_value("warehouse"); + } else { + this.item.warehouse = this.dialog.get_value("warehouse"); + } + this.get_auto_data(); }, get_query: () => { @@ -164,12 +170,14 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { fields.push({ fieldtype: "Section Break", + depends_on: "eval:doc.enter_manually !== 1 || doc.entries?.length > 0", }); fields.push({ fieldname: "entries", fieldtype: "Table", allow_bulk_edit: true, + depends_on: "eval:doc.enter_manually !== 1 || doc.entries?.length > 0", data: [], fields: this.get_dialog_table_fields(), }); @@ -178,6 +186,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { } get_attach_field() { + let me = this; let label = this.item?.has_serial_no ? __("Serial Nos") : __("Batch Nos"); let primary_label = this.bundle ? __("Update") : __("Add"); @@ -185,83 +194,96 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { label = __("Serial Nos / Batch Nos"); } - let fields = [ + let fields = []; + if (this.item.has_serial_no) { + fields.push({ + fieldtype: "Check", + label: __("Enter Manually"), + fieldname: "enter_manually", + default: 1, + depends_on: "eval:doc.import_using_csv_file !== 1", + change() { + if (me.dialog.get_value("enter_manually")) { + me.dialog.set_value("import_using_csv_file", 0); + } + }, + }); + } + + fields = [ + ...fields, + { + fieldtype: "Check", + label: __("Import Using CSV file"), + fieldname: "import_using_csv_file", + depends_on: "eval:doc.enter_manually !== 1", + default: !this.item.has_serial_no ? 1 : 0, + change() { + if (me.dialog.get_value("import_using_csv_file")) { + me.dialog.set_value("enter_manually", 0); + } + }, + }, { fieldtype: "Section Break", + depends_on: "eval:doc.import_using_csv_file === 1", label: __("{0} {1} via CSV File", [primary_label, label]), }, + { + fieldtype: "Button", + fieldname: "download_csv", + label: __("Download CSV Template"), + click: () => this.download_csv_file(), + }, + { + fieldtype: "Column Break", + }, + { + fieldtype: "Attach", + fieldname: "attach_serial_batch_csv", + label: __("Attach CSV File"), + onchange: () => this.upload_csv_file(), + }, ]; if (this.item?.has_serial_no) { fields = [ ...fields, - { - fieldtype: "Check", - label: __("Import Using CSV file"), - fieldname: "import_using_csv_file", - default: 0, - }, { fieldtype: "Section Break", label: __("{0} {1} Manually", [primary_label, label]), - depends_on: "eval:doc.import_using_csv_file === 0", + depends_on: "eval:doc.enter_manually === 1", }, { fieldtype: "Data", - label: __("Enter Serial No Range"), + label: __("Serial No Range"), fieldname: "serial_no_range", - depends_on: "eval:doc.import_using_csv_file === 0", - description: __('Enter "ABC-001::100" for serial nos "ABC-001" to "ABC-100".'), + depends_on: "eval:doc.enter_manually === 1 && !doc.serial_no_series", + description: __('"SN-01::10" for "SN-01" to "SN-10"'), onchange: () => { this.set_serial_nos_from_range(); }, }, + ]; + } + + if (this.item?.has_serial_no) { + fields = [ + ...fields, + { + fieldtype: "Column Break", + depends_on: "eval:doc.enter_manually === 1", + }, { fieldtype: "Small Text", label: __("Enter Serial Nos"), fieldname: "upload_serial_nos", - depends_on: "eval:doc.import_using_csv_file === 0", + depends_on: "eval:doc.enter_manually === 1", description: __("Enter each serial no in a new line"), }, - { - fieldtype: "Column Break", - depends_on: "eval:doc.import_using_csv_file === 0", - }, - { - fieldtype: "Button", - fieldname: "make_serial_nos", - label: __("Create Serial Nos"), - depends_on: "eval:doc.import_using_csv_file === 0", - click: () => { - this.create_serial_nos(); - }, - }, - { - fieldtype: "Section Break", - depends_on: "eval:doc.import_using_csv_file === 1", - }, ]; } - fields = [ - ...fields, - { - fieldtype: "Button", - fieldname: "download_csv", - label: __("Download CSV Template"), - click: () => this.download_csv_file(), - }, - { - fieldtype: "Column Break", - }, - { - fieldtype: "Attach", - fieldname: "attach_serial_batch_csv", - label: __("Attach CSV File"), - onchange: () => this.upload_csv_file(), - }, - ]; - return fields; } @@ -368,8 +390,28 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { ]; } + get_batch_qty(batch_no, callback) { + let warehouse = this.item.s_warehouse || this.item.t_warehouse || this.item.warehouse; + frappe.call({ + method: "erpnext.stock.doctype.batch.batch.get_batch_qty", + args: { + batch_no: batch_no, + warehouse: warehouse, + item_code: this.item.item_code, + posting_date: this.frm.doc.posting_date, + posting_time: this.frm.doc.posting_time, + }, + callback: (r) => { + if (r.message) { + callback(flt(r.message)); + } + }, + }); + } + get_dialog_table_fields() { let fields = []; + let me = this; if (this.item.has_serial_no) { fields.push({ @@ -395,6 +437,20 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { fieldname: "batch_no", label: __("Batch No"), in_list_view: 1, + get_route_options_for_new_doc: () => { + return { + item: this.item.item_code, + }; + }, + change() { + let doc = this.doc; + if (!doc.qty && me.item.type_of_transaction === "Outward") { + me.get_batch_qty(doc.batch_no, (qty) => { + doc.qty = qty; + this.grid.set_value("qty", qty, doc); + }); + } + }, get_query: () => { let is_inward = false; if ( @@ -406,6 +462,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { is_inward = true; } + let include_expired_batches = me.include_expired_batches(); + return { query: "erpnext.controllers.queries.get_batch_no", filters: { @@ -413,6 +471,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { warehouse: this.item.s_warehouse || this.item.t_warehouse || this.item.warehouse, is_inward: is_inward, + include_expired_batches: include_expired_batches, }, }; }, @@ -441,6 +500,14 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { return fields; } + include_expired_batches() { + return ( + this.frm.doc.doctype === "Stock Reconciliation" || + (this.frm.doc.doctype === "Stock Entry" && + ["Material Receipt", "Material Transfer", "Material Issue"].includes(this.frm.doc.purpose)) + ); + } + get_auto_data() { let { qty, based_on } = this.dialog.get_values(); @@ -458,12 +525,17 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { based_on = "FIFO"; } + let warehouse = this.item.warehouse || this.item.s_warehouse; + if (this.item?.is_rejected) { + warehouse = this.item.rejected_warehouse; + } + if (qty) { frappe.call({ method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.get_auto_data", args: { item_code: this.item.item_code, - warehouse: this.item.warehouse || this.item.s_warehouse, + warehouse: warehouse, has_serial_no: this.item.has_serial_no, has_batch_no: this.item.has_batch_no, qty: qty, @@ -482,6 +554,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { scan_barcode_data() { const { scan_serial_no, scan_batch_no } = this.dialog.get_values(); + this.dialog.set_value("enter_manually", 0); + if (scan_serial_no || scan_batch_no) { frappe.call({ method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.is_serial_batch_no_exists", @@ -525,14 +599,13 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { serial_no: scan_serial_no, }, callback: (r) => { - if (r.message) { - this.dialog.fields_dict.entries.df.data.push({ - serial_no: scan_serial_no, - batch_no: r.message, - }); + this.dialog.fields_dict.entries.df.data.push({ + serial_no: scan_serial_no, + batch_no: r.message, + }); - this.dialog.fields_dict.scan_serial_no.set_value(""); - } + this.dialog.fields_dict.scan_serial_no.set_value(""); + this.dialog.fields_dict.entries.grid.refresh(); }, }); } @@ -561,6 +634,12 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { update_bundle_entries() { let entries = this.dialog.get_values().entries; let warehouse = this.dialog.get_value("warehouse"); + let upload_serial_nos = this.dialog.get_value("upload_serial_nos"); + + if (!entries?.length && upload_serial_nos) { + this.create_serial_nos(); + return; + } if ((entries && !entries.length) || !entries) { frappe.throw(__("Please add atleast one Serial No / Batch No")); @@ -570,6 +649,10 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { frappe.throw(__("Please select a Warehouse")); } + if (this.item?.is_rejected && this.item.rejected_warehouse === this.item.warehouse) { + frappe.throw(__("Rejected Warehouse and Accepted Warehouse cannot be same.")); + } + frappe .call({ method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.add_serial_batch_ledgers", @@ -581,9 +664,13 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { }, }) .then((r) => { - this.callback && this.callback(r.message); - this.frm.save(); - this.dialog.hide(); + frappe.run_serially([ + () => { + this.callback && this.callback(r.message); + }, + () => this.frm.save(), + () => this.dialog.hide(), + ]); }); } @@ -607,6 +694,10 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { } get_warehouse() { + if (this.item?.is_rejected) { + return this.item.rejected_warehouse; + } + return this.item?.type_of_transaction === "Outward" ? this.item.warehouse || this.item.s_warehouse : this.item.warehouse || this.item.t_warehouse; @@ -640,5 +731,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { }); this.dialog.fields_dict.entries.grid.refresh(); + if (this.dialog.fields_dict.entries.df.data?.length) { + this.dialog.set_value("enter_manually", 0); + } } }; diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js index 6864e2865d3a..7dba4705e405 100644 --- a/erpnext/public/js/utils/unreconcile.js +++ b/erpnext/public/js/utils/unreconcile.js @@ -4,7 +4,8 @@ erpnext.accounts.unreconcile_payment = { add_unreconcile_btn(frm) { if (frm.doc.docstatus == 1) { if ( - (frm.doc.doctype == "Journal Entry" && frm.doc.voucher_type != "Journal Entry") || + (frm.doc.doctype == "Journal Entry" && + !["Journal Entry", "Bank Entry", "Cash Entry"].includes(frm.doc.voucher_type)) || !["Purchase Invoice", "Sales Invoice", "Journal Entry", "Payment Entry"].includes( frm.doc.doctype ) @@ -69,7 +70,7 @@ erpnext.accounts.unreconcile_payment = { { label: __("Voucher Type"), fieldname: "voucher_type", - fieldtype: "Dynamic Link", + fieldtype: "Link", options: "DocType", in_list_view: 1, read_only: 1, @@ -77,7 +78,7 @@ erpnext.accounts.unreconcile_payment = { { label: __("Voucher No"), fieldname: "voucher_no", - fieldtype: "Link", + fieldtype: "Dynamic Link", options: "voucher_type", in_list_view: 1, read_only: 1, @@ -99,6 +100,7 @@ erpnext.accounts.unreconcile_payment = { fieldtype: "Table", read_only: 1, fields: child_table_fields, + cannot_add_rows: true, }, ]; @@ -122,7 +124,6 @@ erpnext.accounts.unreconcile_payment = { title: "UnReconcile Allocations", fields: unreconcile_dialog_fields, size: "large", - cannot_add_rows: true, primary_action_label: "UnReconcile", primary_action(values) { let selected_allocations = values.allocations.filter((x) => x.__checked); diff --git a/erpnext/public/scss/erpnext.scss b/erpnext/public/scss/erpnext.scss index 03dd31104e1e..29a2696470f4 100644 --- a/erpnext/public/scss/erpnext.scss +++ b/erpnext/public/scss/erpnext.scss @@ -507,6 +507,47 @@ body[data-route="pos"] { position: relative; } +.ring-container { + position: relative; +} + +.circle { + width: 9px; + height: 9px; + background-color: #278f5e; + border-radius: 50%; + position: absolute; + left: 9px; + top: 8px; +} + +@keyframes pulsate { + 0% { + -webkit-transform: scale(0.1, 0.1); + opacity: 0; + } + 50% { + opacity: 1; + } + 100% { + -webkit-transform: scale(1.2, 1.2); + opacity: 0; + } +} + +.ringring { + border: 2px solid #62bd19; + -webkit-border-radius: 40px; + height: 15px; + width: 15px; + position: absolute; + left: 6px; + top: 5px; + -webkit-animation: pulsate 3s ease-out; + -webkit-animation-iteration-count: infinite; + opacity: 0; +} + .plant-floor { padding-bottom: 25px; } diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index af32f4ecf84f..dec09e512fe7 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -21,6 +21,7 @@ "gender", "lead_name", "opportunity_name", + "prospect_name", "account_manager", "image", "defaults_tab", @@ -307,13 +308,15 @@ "fetch_from": "customer_primary_contact.mobile_no", "fieldname": "mobile_no", "fieldtype": "Read Only", - "label": "Mobile No" + "label": "Mobile No", + "options": "Mobile" }, { "fetch_from": "customer_primary_contact.email_id", "fieldname": "email_id", "fieldtype": "Read Only", - "label": "Email Id" + "label": "Email Id", + "options": "Email" }, { "fieldname": "column_break_26", @@ -570,6 +573,14 @@ { "fieldname": "column_break_nwor", "fieldtype": "Column Break" + }, + { + "fieldname": "prospect_name", + "fieldtype": "Link", + "label": "From Prospect", + "no_copy": 1, + "options": "Prospect", + "print_hide": 1 } ], "icon": "fa fa-user", @@ -583,7 +594,7 @@ "link_fieldname": "party" } ], - "modified": "2024-05-08 18:03:20.716169", + "modified": "2024-06-17 03:24:59.612974", "modified_by": "Administrator", "module": "Selling", "name": "Customer", @@ -668,4 +679,4 @@ "states": [], "title_field": "customer_name", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 71f0022df142..0e9c1f3e790c 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -77,6 +77,7 @@ class Customer(TransactionBase): payment_terms: DF.Link | None portal_users: DF.Table[PortalUser] primary_address: DF.Text | None + prospect_name: DF.Link | None represents_company: DF.Link | None sales_team: DF.Table[SalesTeam] salutation: DF.Link | None @@ -104,7 +105,7 @@ def autoname(self): elif cust_master_name == "Naming Series": set_name_by_naming_series(self) else: - self.name = set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self) + set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self) def get_customer_name(self): if frappe.db.get_value("Customer", self.customer_name) and not frappe.flags.in_import: diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py index 464cff01d7e0..609f45a608f7 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/product_bundle.py @@ -5,6 +5,7 @@ import frappe from frappe import _ from frappe.model.document import Document +from frappe.query_builder import Criterion from frappe.utils import get_link_to_form @@ -93,15 +94,24 @@ def validate_child_items(self): def get_new_item_code(doctype, txt, searchfield, start, page_len, filters): product_bundles = frappe.db.get_list("Product Bundle", {"disabled": 0}, pluck="name") + if not searchfield or searchfield == "name": + searchfield = frappe.get_meta("Item").get("search_fields") + + searchfield = searchfield.split(",") + searchfield.append("name") + item = frappe.qb.DocType("Item") query = ( frappe.qb.from_(item) - .select(item.item_code, item.item_name) - .where((item.is_stock_item == 0) & (item.is_fixed_asset == 0) & (item[searchfield].like(f"%{txt}%"))) + .select(item.name, item.item_name) + .where((item.is_stock_item == 0) & (item.is_fixed_asset == 0)) .limit(page_len) .offset(start) ) + if searchfield: + query = query.where(Criterion.any([item[fieldname].like(f"%{txt}%") for fieldname in searchfield])) + if product_bundles: query = query.where(item.name.notin(product_bundles)) diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 3044d865c0ca..7311857e3506 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -24,20 +24,6 @@ frappe.ui.form.on("Quotation", { frm.set_df_property("packed_items", "cannot_add_rows", true); frm.set_df_property("packed_items", "cannot_delete_rows", true); - frm.set_query("company_address", function (doc) { - if (!doc.company) { - frappe.throw(__("Please set Company")); - } - - return { - query: "frappe.contacts.doctype.address.address.address_query", - filters: { - link_doctype: "Company", - link_name: doc.company, - }, - }; - }); - frm.set_query("serial_and_batch_bundle", "packed_items", (doc, cdt, cdn) => { let row = locals[cdt][cdn]; return { @@ -71,7 +57,7 @@ frappe.ui.form.on("Quotation", { frm.trigger("set_label"); frm.trigger("toggle_reqd_lead_customer"); frm.trigger("set_dynamic_field_label"); - frm.set_value("party_name", ""); + // frm.set_value("party_name", ""); // removed to set party_name from url for crm integration frm.set_value("customer_name", ""); }, diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index 982e73267756..4d257ff69e7e 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -65,6 +65,7 @@ "grand_total", "rounding_adjustment", "rounded_total", + "disable_rounded_total", "in_words", "section_break_44", "apply_discount_on", @@ -95,8 +96,9 @@ "shipping_address", "company_address_section", "company_address", - "column_break_87", "company_address_display", + "column_break_87", + "company_contact_person", "terms_tab", "payment_schedule_section", "payment_terms_template", @@ -661,6 +663,7 @@ "width": "200px" }, { + "depends_on": "eval:!doc.disable_rounded_total", "fieldname": "base_rounding_adjustment", "fieldtype": "Currency", "label": "Rounding Adjustment (Company Currency)", @@ -709,6 +712,7 @@ "width": "200px" }, { + "depends_on": "eval:!doc.disable_rounded_total", "fieldname": "rounding_adjustment", "fieldtype": "Currency", "label": "Rounding Adjustment", @@ -1067,13 +1071,26 @@ "fieldname": "named_place", "fieldtype": "Data", "label": "Named Place" + }, + { + "default": "0", + "fieldname": "disable_rounded_total", + "fieldtype": "Check", + "label": "Disable Rounded Total" + }, + { + "fieldname": "company_contact_person", + "fieldtype": "Link", + "label": "Company Contact Person", + "options": "Contact", + "print_hide": 1 } ], "icon": "fa fa-shopping-cart", "idx": 82, "is_submittable": 1, "links": [], - "modified": "2024-03-20 16:04:21.567847", + "modified": "2024-11-26 12:43:29.293637", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index f9442204df15..55a14c912c2e 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -50,6 +50,7 @@ class Quotation(SellingController): company: DF.Link company_address: DF.Link | None company_address_display: DF.SmallText | None + company_contact_person: DF.Link | None competitors: DF.TableMultiSelect[CompetitorDetail] contact_display: DF.SmallText | None contact_email: DF.Data | None @@ -61,6 +62,7 @@ class Quotation(SellingController): customer_address: DF.Link | None customer_group: DF.Link | None customer_name: DF.Data | None + disable_rounded_total: DF.Check discount_amount: DF.Currency enq_det: DF.Text | None grand_total: DF.Currency @@ -220,6 +222,10 @@ def set_customer_name(self): "Lead", self.party_name, ["lead_name", "company_name"] ) self.customer_name = company_name or lead_name + elif self.party_name and self.quotation_to == "Prospect": + self.customer_name = self.party_name + elif self.party_name and self.quotation_to == "CRM Deal": + self.customer_name = frappe.db.get_value("CRM Deal", self.party_name, "organization") def update_opportunity(self, status): for opportunity in set(d.prevdoc_docname for d in self.get("items")): @@ -347,8 +353,8 @@ def make_sales_order(source_name: str, target_doc=None): return _make_sales_order(source_name, target_doc) -def _make_sales_order(source_name, target_doc=None, customer_group=None, ignore_permissions=False): - customer = _make_customer(source_name, ignore_permissions, customer_group) +def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): + customer = _make_customer(source_name, ignore_permissions) ordered_items = frappe._dict( frappe.db.get_all( "Sales Order Item", @@ -365,24 +371,25 @@ def set_missing_values(source, target): if customer: target.customer = customer.name target.customer_name = customer.customer_name + + # sales team + if not target.get("sales_team"): + for d in customer.get("sales_team") or []: + target.append( + "sales_team", + { + "sales_person": d.sales_person, + "allocated_percentage": d.allocated_percentage or None, + "commission_rate": d.commission_rate, + }, + ) + if source.referral_sales_partner: target.sales_partner = source.referral_sales_partner target.commission_rate = frappe.get_value( "Sales Partner", source.referral_sales_partner, "commission_rate" ) - # sales team - if not target.get("sales_team"): - for d in customer.get("sales_team") or []: - target.append( - "sales_team", - { - "sales_person": d.sales_person, - "allocated_percentage": d.allocated_percentage or None, - "commission_rate": d.commission_rate, - }, - ) - target.flags.ignore_permissions = ignore_permissions target.run_method("set_missing_values") target.run_method("calculate_taxes_and_totals") @@ -426,7 +433,7 @@ def can_map_row(item) -> bool: "postprocess": update_item, "condition": can_map_row, }, - "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, + "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True}, "Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, "Payment Schedule": {"doctype": "Payment Schedule", "add_if_empty": True}, }, @@ -491,7 +498,7 @@ def update_item(obj, target, source_parent): "postprocess": update_item, "condition": lambda row: not row.is_alternative, }, - "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, + "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True}, "Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, }, target_doc, @@ -502,51 +509,71 @@ def update_item(obj, target, source_parent): return doclist -def _make_customer(source_name, ignore_permissions=False, customer_group=None): +def _make_customer(source_name, ignore_permissions=False): quotation = frappe.db.get_value( - "Quotation", source_name, ["order_type", "party_name", "customer_name"], as_dict=1 + "Quotation", + source_name, + ["order_type", "quotation_to", "party_name", "customer_name"], + as_dict=1, ) - if quotation and quotation.get("party_name"): - if not frappe.db.exists("Customer", quotation.get("party_name")): - lead_name = quotation.get("party_name") - customer_name = frappe.db.get_value( - "Customer", {"lead_name": lead_name}, ["name", "customer_name"], as_dict=True - ) - if not customer_name: - from erpnext.crm.doctype.lead.lead import _make_customer - - customer_doclist = _make_customer(lead_name, ignore_permissions=ignore_permissions) - customer = frappe.get_doc(customer_doclist) - customer.flags.ignore_permissions = ignore_permissions - customer.customer_group = customer_group - - try: - customer.insert() - return customer - except frappe.NameError: - if frappe.defaults.get_global_default("cust_master_name") == "Customer Name": - customer.run_method("autoname") - customer.name += "-" + lead_name - customer.insert() - return customer - else: - raise - except frappe.MandatoryError as e: - mandatory_fields = e.args[0].split(":")[1].split(",") - mandatory_fields = [customer.meta.get_label(field.strip()) for field in mandatory_fields] - - frappe.local.message_log = [] - lead_link = frappe.utils.get_link_to_form("Lead", lead_name) - message = ( - _("Could not auto create Customer due to the following missing mandatory field(s):") - + "
" - ) - message += "
  • " + "
  • ".join(mandatory_fields) + "
" - message += _("Please create Customer from Lead {0}.").format(lead_link) + if quotation.quotation_to == "Customer": + return frappe.get_doc("Customer", quotation.party_name) - frappe.throw(message, title=_("Mandatory Missing")) - else: - return customer_name - else: - return frappe.get_doc("Customer", quotation.get("party_name")) + # Check if a Customer already exists for the Lead or Prospect. + existing_customer = None + if quotation.quotation_to == "Lead": + existing_customer = frappe.db.get_value("Customer", {"lead_name": quotation.party_name}) + elif quotation.quotation_to == "Prospect": + existing_customer = frappe.db.get_value("Customer", {"prospect_name": quotation.party_name}) + + if existing_customer: + return frappe.get_doc("Customer", existing_customer) + + # If no Customer exists, create a new Customer or Prospect. + if quotation.quotation_to == "Lead": + return create_customer_from_lead(quotation.party_name, ignore_permissions=ignore_permissions) + elif quotation.quotation_to == "Prospect": + return create_customer_from_prospect(quotation.party_name, ignore_permissions=ignore_permissions) + + return None + + +def create_customer_from_lead(lead_name, ignore_permissions=False): + from erpnext.crm.doctype.lead.lead import _make_customer + + customer = _make_customer(lead_name, ignore_permissions=ignore_permissions) + customer.flags.ignore_permissions = ignore_permissions + + try: + customer.insert() + return customer + except frappe.MandatoryError as e: + handle_mandatory_error(e, customer, lead_name) + + +def create_customer_from_prospect(prospect_name, ignore_permissions=False): + from erpnext.crm.doctype.prospect.prospect import make_customer as make_customer_from_prospect + + customer = make_customer_from_prospect(prospect_name) + customer.flags.ignore_permissions = ignore_permissions + + try: + customer.insert() + return customer + except frappe.MandatoryError as e: + handle_mandatory_error(e, customer, prospect_name) + + +def handle_mandatory_error(e, customer, lead_name): + from frappe.utils import get_link_to_form + + mandatory_fields = e.args[0].split(":")[1].split(",") + mandatory_fields = [_(customer.meta.get_label(field.strip())) for field in mandatory_fields] + + frappe.local.message_log = [] + message = _("Could not auto create Customer due to the following missing mandatory field(s):") + "
" + message += "
  • " + "
  • ".join(mandatory_fields) + "
" + message += _("Please create Customer from Lead {0}.").format(get_link_to_form("Lead", lead_name)) + + frappe.throw(message, title=_("Mandatory Missing")) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 7b66b7a251ad..05f43f265591 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -561,12 +561,50 @@ def test_alternative_items_with_service_items(self): "description": "VAT", "doctype": "Sales Taxes and Charges", "rate": 10, + "included_in_print_rate": 1, }, ) quotation.submit() - self.assertEqual(quotation.net_total, 290) - self.assertEqual(quotation.grand_total, 319) + self.assertEqual(round(quotation.items[1].net_rate, 2), 136.36) + self.assertEqual(round(quotation.items[1].amount, 2), 150) + + self.assertEqual(round(quotation.items[2].net_rate, 2), 163.64) + self.assertEqual(round(quotation.items[2].amount, 2), 180) + + self.assertEqual(round(quotation.net_total, 2), 263.64) + self.assertEqual(round(quotation.total_taxes_and_charges, 2), 26.36) + self.assertEqual(quotation.grand_total, 290) + + def test_amount_calculation_for_alternative_items(self): + """Make sure that the amount is calculated correctly for alternative items when the qty is changed.""" + from erpnext.stock.doctype.item.test_item import make_item + + item_list = [] + stock_items = { + "_Test Simple Item 1": 100, + "_Test Alt 1": 120, + } + + for item, rate in stock_items.items(): + make_item(item, {"is_stock_item": 0}) + item_list.append( + { + "item_code": item, + "qty": 1, + "rate": rate, + "is_alternative": "Alt" in item, + } + ) + + quotation = make_quotation(item_list=item_list, do_not_submit=1) + + self.assertEqual(quotation.items[1].amount, 120) + + quotation.items[1].qty = 2 + quotation.save() + + self.assertEqual(quotation.items[1].amount, 240) def test_alternative_items_sales_order_mapping_with_stock_items(self): from erpnext.selling.doctype.quotation.quotation import make_sales_order @@ -677,6 +715,20 @@ def test_item_tax_template_for_quotation(self): item_doc.taxes = [] item_doc.save() + def test_grand_total_and_rounded_total_values(self): + quotation = make_quotation(qty=6, rate=12.3, do_not_submit=1) + + self.assertEqual(quotation.grand_total, 73.8) + self.assertEqual(quotation.rounding_adjustment, 0.2) + self.assertEqual(quotation.rounded_total, 74) + + quotation.disable_rounded_total = 1 + quotation.save() + + self.assertEqual(quotation.grand_total, 73.8) + self.assertEqual(quotation.rounding_adjustment, 0) + self.assertEqual(quotation.rounded_total, 0) + test_records = frappe.get_test_records("Quotation") diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json index 0e25313f76ad..1ea19aaaf560 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.json +++ b/erpnext/selling/doctype/quotation_item/quotation_item.json @@ -24,6 +24,10 @@ "uom", "conversion_factor", "stock_qty", + "available_quantity_section", + "actual_qty", + "column_break_ylrv", + "company_total_stock", "section_break_16", "price_list_rate", "base_price_list_rate", @@ -70,7 +74,6 @@ "prevdoc_docname", "item_balance", "projected_qty", - "actual_qty", "col_break4", "stock_balance", "item_tax_rate", @@ -460,9 +463,10 @@ "report_hide": 1 }, { + "allow_on_submit": 1, "fieldname": "actual_qty", "fieldtype": "Float", - "label": "Actual Qty", + "label": "Qty (Warehouse)", "no_copy": 1, "print_hide": 1, "read_only": 1, @@ -662,12 +666,31 @@ "label": "Has Alternative Item", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "available_quantity_section", + "fieldtype": "Section Break", + "label": "Available Quantity" + }, + { + "fieldname": "column_break_ylrv", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "fieldname": "company_total_stock", + "fieldtype": "Float", + "label": "Qty (Company)", + "no_copy": 1, + "print_hide": 1, + "read_only": 1, + "report_hide": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:24:24.619832", + "modified": "2024-11-24 15:18:43.952844", "modified_by": "Administrator", "module": "Selling", "name": "Quotation Item", @@ -677,4 +700,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.py b/erpnext/selling/doctype/quotation_item/quotation_item.py index f209762c3ba6..7d68eaf07bab 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.py +++ b/erpnext/selling/doctype/quotation_item/quotation_item.py @@ -27,6 +27,7 @@ class QuotationItem(Document): blanket_order: DF.Link | None blanket_order_rate: DF.Currency brand: DF.Link | None + company_total_stock: DF.Float conversion_factor: DF.Float customer_item_code: DF.Data | None description: DF.TextEditor | None diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index cdcd1047bd89..de4053458e47 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -26,20 +26,6 @@ frappe.ui.form.on("Sales Order", { return doc.stock_qty <= doc.delivered_qty ? "green" : "orange"; }); - frm.set_query("company_address", function (doc) { - if (!doc.company) { - frappe.throw(__("Please set Company")); - } - - return { - query: "frappe.contacts.doctype.address.address.address_query", - filters: { - link_doctype: "Company", - link_name: doc.company, - }, - }; - }); - frm.set_query("bom_no", "items", function (doc, cdt, cdn) { var row = locals[cdt][cdn]; return { @@ -57,8 +43,8 @@ frappe.ui.form.on("Sales Order", { if (frm.doc.docstatus === 1) { if ( frm.doc.status !== "Closed" && - flt(frm.doc.per_delivered, 2) < 100 && - flt(frm.doc.per_billed, 2) < 100 && + flt(frm.doc.per_delivered) < 100 && + flt(frm.doc.per_billed) < 100 && frm.has_perm("write") ) { frm.add_custom_button(__("Update Items"), () => { @@ -160,7 +146,7 @@ frappe.ui.form.on("Sales Order", { target: frm, setters: [ { - label: "Supplier", + label: __("Supplier"), fieldname: "supplier", fieldtype: "Link", options: "Supplier", @@ -604,7 +590,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex __("Status") ); - if (flt(doc.per_delivered, 2) < 100 || flt(doc.per_billed, 2) < 100) { + if (flt(doc.per_delivered) < 100 || flt(doc.per_billed) < 100) { // close this.frm.add_custom_button(__("Close"), () => this.close_sales_order(), __("Status")); } @@ -627,7 +613,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex ) && !this.frm.doc.skip_delivery_note; if (this.frm.has_perm("submit")) { - if (flt(doc.per_delivered, 2) < 100 || flt(doc.per_billed, 2) < 100) { + if (flt(doc.per_delivered) < 100 || flt(doc.per_billed) < 100) { // hold this.frm.add_custom_button( __("Hold"), @@ -645,8 +631,8 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex if ( (!doc.__onload || !doc.__onload.has_reserved_stock) && - flt(doc.per_picked, 2) < 100 && - flt(doc.per_delivered, 2) < 100 && + flt(doc.per_picked) < 100 && + flt(doc.per_delivered) < 100 && frappe.model.can_create("Pick List") ) { this.frm.add_custom_button( @@ -664,7 +650,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex // delivery note if ( - flt(doc.per_delivered, 2) < 100 && + flt(doc.per_delivered) < 100 && (order_is_a_sale || order_is_a_custom_sale) && allow_delivery ) { @@ -686,7 +672,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex } // sales invoice - if (flt(doc.per_billed, 2) < 100 && frappe.model.can_create("Sales Invoice")) { + if (flt(doc.per_billed) < 100 && frappe.model.can_create("Sales Invoice")) { this.frm.add_custom_button( __("Sales Invoice"), () => me.make_sales_invoice(), @@ -697,8 +683,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex // material request if ( (!doc.order_type || - ((order_is_a_sale || order_is_a_custom_sale) && - flt(doc.per_delivered, 2) < 100)) && + ((order_is_a_sale || order_is_a_custom_sale) && flt(doc.per_delivered) < 100)) && frappe.model.can_create("Material Request") ) { this.frm.add_custom_button( @@ -723,7 +708,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex } // maintenance - if (flt(doc.per_delivered, 2) < 100 && (order_is_maintenance || order_is_a_custom_sale)) { + if (flt(doc.per_delivered) < 100 && (order_is_maintenance || order_is_a_custom_sale)) { if (frappe.model.can_create("Maintenance Visit")) { this.frm.add_custom_button( __("Maintenance Visit"), @@ -741,7 +726,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex } // project - if (flt(doc.per_delivered, 2) < 100 && frappe.model.can_create("Project")) { + if (flt(doc.per_delivered) < 100 && frappe.model.can_create("Project")) { this.frm.add_custom_button(__("Project"), () => this.make_project(), __("Create")); } @@ -769,10 +754,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex } } // payment request - if ( - flt(doc.per_billed, precision("per_billed", doc)) < - 100 + frappe.boot.sysdefaults.over_billing_allowance - ) { + if (flt(doc.per_billed) < 100 + frappe.boot.sysdefaults.over_billing_allowance) { this.frm.add_custom_button( __("Payment Request"), () => this.make_payment_request(), @@ -801,7 +783,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex target: me.frm, setters: [ { - label: "Customer", + label: __("Customer"), fieldname: "party_name", fieldtype: "Link", options: "Customer", @@ -856,7 +838,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex } else { const fields = [ { - label: "Items", + label: __("Items"), fieldtype: "Table", fieldname: "items", description: __("Select BOM and Qty for Production"), @@ -1211,7 +1193,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex { fieldname: "items_for_po", fieldtype: "Table", - label: "Select Items", + label: __("Select Items"), fields: [ { fieldtype: "Data", @@ -1251,7 +1233,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex ], }, ], - primary_action_label: "Create Purchase Order", + primary_action_label: __("Create Purchase Order"), primary_action(args) { if (!args) return; diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 0695c3fd9c4a..1525b9632de5 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -113,8 +113,9 @@ "dispatch_address", "col_break46", "company_address", - "column_break_92", "company_address_display", + "column_break_92", + "company_contact_person", "payment_schedule_section", "payment_terms_section", "payment_terms_template", @@ -1640,13 +1641,20 @@ "no_copy": 1, "print_hide": 1, "report_hide": 1 + }, + { + "fieldname": "company_contact_person", + "fieldtype": "Link", + "label": "Company Contact Person", + "options": "Contact", + "print_hide": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2024-05-23 16:35:54.905804", + "modified": "2024-11-26 12:42:06.872527", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 570677aad789..374c37f99bf4 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -35,7 +35,7 @@ get_sre_reserved_qty_details_for_voucher, has_reserved_stock, ) -from erpnext.stock.get_item_details import get_default_bom, get_price_list_rate +from erpnext.stock.get_item_details import get_bin_details, get_default_bom, get_price_list_rate from erpnext.stock.stock_balance import get_reserved_qty, update_bin_qty form_grid_templates = {"items": "templates/form_grid/item_grid.html"} @@ -85,6 +85,7 @@ class SalesOrder(SellingController): company: DF.Link company_address: DF.Link | None company_address_display: DF.SmallText | None + company_contact_person: DF.Link | None contact_display: DF.SmallText | None contact_email: DF.Data | None contact_mobile: DF.SmallText | None @@ -185,6 +186,8 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def onload(self) -> None: + super().onload() + if frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"): if self.has_unreserved_stock(): self.set_onload("has_unreserved_stock", True) @@ -582,7 +585,7 @@ def update_delivery_status(self): item_delivered_qty = item_delivered_qty[0][0] if item_delivered_qty else 0 item.db_set("delivered_qty", flt(item_delivered_qty), update_modified=False) - delivered_qty += item.delivered_qty + delivered_qty += min(item.delivered_qty, item.qty) tot_qty += item.qty if tot_qty != 0: @@ -836,6 +839,9 @@ def update_item(source, target, source_parent): target.project = source_parent.project target.qty = get_remaining_qty(source) target.stock_qty = flt(target.qty) * flt(target.conversion_factor) + target.actual_qty = get_bin_details( + target.item_code, target.warehouse, source_parent.company, True + ).get("actual_qty", 0) args = target.as_dict().copy() args.update( @@ -931,7 +937,7 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None): mapper = { "Sales Order": {"doctype": "Delivery Note", "validation": {"docstatus": ["=", 1]}}, - "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, + "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True}, "Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, } @@ -1123,7 +1129,10 @@ def update_item(source, target, source_parent): "condition": lambda doc: doc.qty and (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)), }, - "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, + "Sales Taxes and Charges": { + "doctype": "Sales Taxes and Charges", + "reset_value": True, + }, "Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, }, target_doc, @@ -1222,7 +1231,10 @@ def get_events(start, end, filters=None): """, {"start": start, "end": end}, as_dict=True, - update={"allDay": 0}, + update={ + "allDay": 0, + "convertToUserTz": 0, + }, ) return data @@ -1339,6 +1351,8 @@ def update_item(source, target, source_parent): "discount_percentage", "discount_amount", "pricing_rules", + "margin_type", + "margin_rate_or_amount", ], "postprocess": update_item, "condition": lambda doc: doc.ordered_qty < doc.stock_qty @@ -1392,9 +1406,17 @@ def set_missing_values(source, target): target.payment_schedule = [] if is_drop_ship_order(target): - target.customer = source.customer - target.customer_name = source.customer_name - target.shipping_address = source.shipping_address_name + if source.shipping_address_name: + target.shipping_address = source.shipping_address_name + target.shipping_address_display = source.shipping_address + else: + target.shipping_address = source.customer_address + target.shipping_address_display = source.address_display + + target.customer_contact_person = source.contact_person + target.customer_contact_display = source.contact_display + target.customer_contact_mobile = source.contact_mobile + target.customer_contact_email = source.contact_email else: target.customer = target.customer_name = target.shipping_address = None diff --git a/erpnext/selling/doctype/sales_order/sales_order_calendar.js b/erpnext/selling/doctype/sales_order/sales_order_calendar.js index f4c0e2ba72a9..59a32bde7a3b 100644 --- a/erpnext/selling/doctype/sales_order/sales_order_calendar.js +++ b/erpnext/selling/doctype/sales_order/sales_order_calendar.js @@ -8,6 +8,7 @@ frappe.views.calendar["Sales Order"] = { id: "name", title: "customer_name", allDay: "allDay", + convertToUserTz: "convertToUserTz", }, gantt: true, filters: [ diff --git a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py index c84009725b80..7c1c0deb33f0 100644 --- a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py +++ b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py @@ -15,6 +15,8 @@ def get_data(): }, "internal_links": { "Quotation": ["items", "prevdoc_docname"], + "BOM": ["items", "bom_no"], + "Blanket Order": ["items", "blanket_order"], }, "transactions": [ { @@ -23,7 +25,7 @@ def get_data(): }, {"label": _("Purchasing"), "items": ["Material Request", "Purchase Order"]}, {"label": _("Projects"), "items": ["Project"]}, - {"label": _("Manufacturing"), "items": ["Work Order"]}, + {"label": _("Manufacturing"), "items": ["Work Order", "BOM", "Blanket Order"]}, {"label": _("Reference"), "items": ["Quotation", "Auto Repeat", "Stock Reservation Entry"]}, {"label": _("Payment"), "items": ["Payment Entry", "Payment Request", "Journal Entry"]}, ], diff --git a/erpnext/selling/doctype/sales_order/sales_order_list.js b/erpnext/selling/doctype/sales_order/sales_order_list.js index 5ccd5d551cb8..46d115a17135 100644 --- a/erpnext/selling/doctype/sales_order/sales_order_list.js +++ b/erpnext/selling/doctype/sales_order/sales_order_list.js @@ -20,14 +20,14 @@ frappe.listview_settings["Sales Order"] = { return [__("On Hold"), "orange", "status,=,On Hold"]; } else if (doc.status === "Completed") { return [__("Completed"), "green", "status,=,Completed"]; - } else if (!doc.skip_delivery_note && flt(doc.per_delivered, 2) < 100) { + } else if (!doc.skip_delivery_note && flt(doc.per_delivered) < 100) { if (frappe.datetime.get_diff(doc.delivery_date) < 0) { // not delivered & overdue return [__("Overdue"), "red", "per_delivered,<,100|delivery_date,<,Today|status,!=,Closed"]; } else if (flt(doc.grand_total) === 0) { // not delivered (zeroount order) return [__("To Deliver"), "orange", "per_delivered,<,100|grand_total,=,0|status,!=,Closed"]; - } else if (flt(doc.per_billed, 2) < 100) { + } else if (flt(doc.per_billed) < 100) { // not delivered & not billed return [ __("To Deliver and Bill"), @@ -39,13 +39,13 @@ frappe.listview_settings["Sales Order"] = { return [__("To Deliver"), "orange", "per_delivered,<,100|per_billed,=,100|status,!=,Closed"]; } } else if ( - flt(doc.per_delivered, 2) === 100 && + flt(doc.per_delivered) === 100 && flt(doc.grand_total) !== 0 && - flt(doc.per_billed, 2) < 100 + flt(doc.per_billed) < 100 ) { // to bill return [__("To Bill"), "orange", "per_delivered,=,100|per_billed,<,100|status,!=,Closed"]; - } else if (doc.skip_delivery_note && flt(doc.per_billed, 2) < 100) { + } else if (doc.skip_delivery_note && flt(doc.per_billed) < 100) { return [__("To Bill"), "orange", "per_billed,<,100|status,!=,Closed"]; } }, diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 53c629a90b46..244a6b1ddadd 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -30,6 +30,7 @@ ) from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.get_item_details import get_bin_details class TestSalesOrder(AccountsTestMixin, FrappeTestCase): @@ -96,6 +97,12 @@ def test_make_material_request(self): self.assertEqual(mr.material_request_type, "Purchase") self.assertEqual(len(mr.get("items")), len(so.get("items"))) + for item in mr.get("items"): + actual_qty = get_bin_details(item.item_code, item.warehouse, mr.company, True).get( + "actual_qty", 0 + ) + self.assertEqual(flt(item.actual_qty), actual_qty) + def test_make_delivery_note(self): so = make_sales_order(do_not_submit=True) diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index d451768eaab2..fb9e895ccb7b 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -78,11 +78,14 @@ "against_blanket_order", "blanket_order", "blanket_order_rate", + "available_quantity_section", + "actual_qty", + "column_break_jpky", + "company_total_stock", "manufacturing_section_section", "bom_no", "planning_section", "projected_qty", - "actual_qty", "ordered_qty", "planned_qty", "production_plan_qty", @@ -636,7 +639,7 @@ "allow_on_submit": 1, "fieldname": "actual_qty", "fieldtype": "Float", - "label": "Actual Qty", + "label": "Qty (Warehouse)", "no_copy": 1, "print_hide": 1, "print_width": "70px", @@ -905,12 +908,30 @@ "label": "Is Stock Item", "print_hide": 1, "report_hide": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "company_total_stock", + "fieldtype": "Float", + "label": "Qty (Company)", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_jpky", + "fieldtype": "Column Break" + }, + { + "fieldname": "available_quantity_section", + "fieldtype": "Section Break", + "label": "Available Quantity" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2024-03-21 18:15:56.625005", + "modified": "2024-11-21 14:21:29.743474", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", @@ -921,4 +942,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.py b/erpnext/selling/doctype/sales_order_item/sales_order_item.py index fa7b9b968f33..888ea755e2e3 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.py +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.py @@ -30,6 +30,7 @@ class SalesOrderItem(Document): blanket_order_rate: DF.Currency bom_no: DF.Link | None brand: DF.Link | None + company_total_stock: DF.Float conversion_factor: DF.Float customer_item_code: DF.Data | None delivered_by_supplier: DF.Check diff --git a/erpnext/selling/number_card/active_customers/active_customers.json b/erpnext/selling/number_card/active_customers/active_customers.json index 33776348477a..7a31a21f6fd7 100644 --- a/erpnext/selling/number_card/active_customers/active_customers.json +++ b/erpnext/selling/number_card/active_customers/active_customers.json @@ -3,7 +3,6 @@ "docstatus": 0, "doctype": "Number Card", "document_type": "Customer", - "dynamic_filters_json": "", "filters_json": "[[\"Customer\",\"disabled\",\"=\",\"0\"]]", "function": "Count", "idx": 0, diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 2bb61a6439c7..206e51bbc5a9 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -35,6 +35,7 @@ def search_by_term(search_term, warehouse, price_list): "description": item_doc.description, "is_stock_item": item_doc.is_stock_item, "item_code": item_doc.name, + "item_group": item_doc.item_group, "item_image": item_doc.image, "item_name": item_doc.item_name, "serial_no": serial_no, @@ -92,6 +93,14 @@ def __sort(p): return {"items": [item]} +def filter_result_items(result, pos_profile): + if result and result.get("items"): + pos_item_groups = frappe.db.get_all("POS Item Group", {"parent": pos_profile}, pluck="item_group") + if not pos_item_groups: + return + result["items"] = [item for item in result.get("items") if item.get("item_group") in pos_item_groups] + + @frappe.whitelist() def get_items(start, page_length, price_list, item_group, pos_profile, search_term=""): warehouse, hide_unavailable_items = frappe.db.get_value( @@ -102,6 +111,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te if search_term: result = search_by_term(search_term, warehouse, price_list) or [] + filter_result_items(result, pos_profile) if result: return result @@ -159,6 +169,8 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te if not items_data: return result + current_date = frappe.utils.today() + for item in items_data: uoms = frappe.get_doc("Item", item.item_code).get("uoms", []) @@ -167,12 +179,16 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te item_price = frappe.get_all( "Item Price", - fields=["price_list_rate", "currency", "uom", "batch_no"], + fields=["price_list_rate", "currency", "uom", "batch_no", "valid_from", "valid_upto"], filters={ "price_list": price_list, "item_code": item.item_code, "selling": True, + "valid_from": ["<=", current_date], + "valid_upto": ["in", [None, "", current_date]], }, + order_by="valid_from desc", + limit=1, ) if not item_price: diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 000a79b70a02..c1127c39cc04 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -30,7 +30,7 @@ erpnext.PointOfSale.Controller = class { fieldname: "mode_of_payment", fieldtype: "Link", in_list_view: 1, - label: "Mode of Payment", + label: __("Mode of Payment"), options: "Mode of Payment", reqd: 1, }, @@ -38,7 +38,7 @@ erpnext.PointOfSale.Controller = class { fieldname: "opening_amount", fieldtype: "Currency", in_list_view: 1, - label: "Opening Amount", + label: __("Opening Amount"), options: "company:company_currency", change: function () { dialog.fields_dict.balance_details.df.data.some((d) => { @@ -87,7 +87,7 @@ erpnext.PointOfSale.Controller = class { { fieldname: "balance_details", fieldtype: "Table", - label: "Opening Balance Details", + label: __("Opening Balance Details"), cannot_add_rows: false, in_place_edit: true, reqd: 1, @@ -547,6 +547,8 @@ erpnext.PointOfSale.Controller = class { async on_cart_update(args) { frappe.dom.freeze(); + if (this.frm.doc.set_warehouse != this.settings.warehouse) + this.frm.doc.set_warehouse = this.settings.warehouse; let item_row = undefined; try { let { field, value, item } = args; @@ -554,7 +556,7 @@ erpnext.PointOfSale.Controller = class { const item_row_exists = !$.isEmptyObject(item_row); const from_selector = field === "qty" && value === "+1"; - if (from_selector) value = flt(item_row.stock_qty) + flt(value); + if (from_selector) value = flt(item_row.qty) + flt(value); if (item_row_exists) { if (field === "qty") value = flt(value); @@ -685,7 +687,7 @@ erpnext.PointOfSale.Controller = class { const is_stock_item = resp[1]; frappe.dom.unfreeze(); - const bold_uom = item_row.uom.bold(); + const bold_uom = item_row.stock_uom.bold(); const bold_item_code = item_row.item_code.bold(); const bold_warehouse = warehouse.bold(); const bold_available_qty = available_qty.toString().bold(); diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index 694f70d4db5e..6342b237f6e0 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -389,28 +389,14 @@ erpnext.PointOfSale.ItemCart = class { placeholder: discount ? discount + "%" : __("Enter discount percentage."), input_class: "input-xs", onchange: function () { - if (flt(this.value) != 0) { - frappe.model.set_value( - frm.doc.doctype, - frm.doc.name, - "additional_discount_percentage", - flt(this.value) - ); - me.hide_discount_control(this.value); - } else { - frappe.model.set_value( - frm.doc.doctype, - frm.doc.name, - "additional_discount_percentage", - 0 - ); - me.$add_discount_elem.css({ - border: "1px dashed var(--gray-500)", - padding: "var(--padding-sm) var(--padding-md)", - }); - me.$add_discount_elem.html(`${me.get_discount_icon()} ${__("Add Discount")}`); - me.discount_field = undefined; - } + this.value = flt(this.value); + frappe.model.set_value( + frm.doc.doctype, + frm.doc.name, + "additional_discount_percentage", + flt(this.value) + ); + me.hide_discount_control(this.value); }, }, parent: this.$add_discount_elem.find(".add-discount-field"), @@ -421,9 +407,13 @@ erpnext.PointOfSale.ItemCart = class { } hide_discount_control(discount) { - if (!discount) { - this.$add_discount_elem.css({ padding: "0px", border: "none" }); - this.$add_discount_elem.html(`
`); + if (!flt(discount)) { + this.$add_discount_elem.css({ + border: "1px dashed var(--gray-500)", + padding: "var(--padding-sm) var(--padding-md)", + }); + this.$add_discount_elem.html(`${this.get_discount_icon()} ${__("Add Discount")}`); + this.discount_field = undefined; } else { this.$add_discount_elem.css({ border: "1px dashed var(--dark-green-500)", @@ -976,13 +966,15 @@ erpnext.PointOfSale.ItemCart = class { if (!res.length) { transaction_container.html( - `
No recent transactions found
` + `
${__("No recent transactions found")}
` ); return; } const elapsed_time = moment(res[0].posting_date + " " + res[0].posting_time).fromNow(); - this.$customer_section.find(".customer-desc").html(`Last transacted ${elapsed_time}`); + this.$customer_section + .find(".customer-desc") + .html(`${__("Last transacted")} ${__(elapsed_time)}`); res.forEach((invoice) => { const posting_datetime = moment(invoice.posting_date + " " + invoice.posting_time).format( @@ -1007,7 +999,7 @@ erpnext.PointOfSale.ItemCart = class {
- ${invoice.status} + ${__(invoice.status)}
@@ -1051,6 +1043,7 @@ erpnext.PointOfSale.ItemCart = class { this.highlight_checkout_btn(false); } + this.hide_discount_control(frm.doc.additional_discount_percentage); this.update_totals_section(frm); if (frm.doc.docstatus === 1) { diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index 4673eaa98584..ad4b4cd15be3 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -272,7 +272,7 @@ erpnext.PointOfSale.ItemDetails = class { }; this.warehouse_control.df.get_query = () => { return { - filters: { company: this.events.get_frm().doc.company }, + filters: { company: this.events.get_frm().doc.company, is_group: 0 }, }; }; this.warehouse_control.refresh(); diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index b5fa8849d604..207a444218ba 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -99,7 +99,7 @@ erpnext.PointOfSale.ItemSelector = class { return `
${qty_to_display}
-
+
{ this.print_receipt(); @@ -112,7 +112,7 @@ erpnext.PointOfSale.PastOrderSummary = class { get_discount_html(doc) { if (doc.discount_amount) { return `
-
Discount (${doc.additional_discount_percentage} %)
+
${__("Discount")} (${doc.additional_discount_percentage} %)
${format_currency(doc.discount_amount, doc.currency)}
`; } else { diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 232b6a02123f..bea1918fa201 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -350,6 +350,11 @@ erpnext.PointOfSale.Payment = class { } checkout() { + const frm = this.events.get_frm(); + frm.cscript.calculate_outstanding_amount(); + frm.refresh_field("outstanding_amount"); + frm.refresh_field("paid_amount"); + frm.refresh_field("base_paid_amount"); this.events.toggle_other_sections(true); this.toggle_component(true); @@ -584,7 +589,7 @@ erpnext.PointOfSale.Payment = class { const remaining = grand_total - doc.paid_amount; const change = doc.change_amount || remaining <= 0 ? -1 * remaining : undefined; const currency = doc.currency; - const label = change ? __("Change") : __("To Be Paid"); + const label = __("Change Amount"); this.$totals.html( `
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 cf61a0e35f3b..1b57a6d7390e 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 @@ -270,11 +270,11 @@ def prepare_chart(s_orders): "labels": [term.payment_term for term in s_orders], "datasets": [ { - "name": "Payment Amount", + "name": _("Payment Amount"), "values": [x.base_payment_amount for x in s_orders], }, { - "name": "Paid Amount", + "name": _("Paid Amount"), "values": [x.paid_amount for x in s_orders], }, ], diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.js b/erpnext/selling/report/sales_analytics/sales_analytics.js index a01103afb96d..7c5d5436877d 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.js +++ b/erpnext/selling/report/sales_analytics/sales_analytics.js @@ -73,6 +73,11 @@ frappe.query_reports["Sales Analytics"] = { default: "Monthly", reqd: 1, }, + { + fieldname: "show_aggregate_value_from_subsidiary_companies", + label: __("Show Aggregate Value from Subsidiary Companies"), + fieldtype: "Check", + }, ], get_datatable_options(options) { return Object.assign(options, { diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py index 27d2e6e555ec..262687ef19db 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.py +++ b/erpnext/selling/report/sales_analytics/sales_analytics.py @@ -4,6 +4,8 @@ import frappe from frappe import _, scrub +from frappe.query_builder import DocType +from frappe.query_builder.functions import IfNull from frappe.utils import add_days, add_to_date, flt, getdate from erpnext.accounts.utils import get_fiscal_year @@ -37,7 +39,26 @@ def __init__(self, filters=None): ] self.get_period_date_ranges() + def update_company_list_for_parent_company(self): + company_list = [self.filters.get("company")] + + selected_company = self.filters.get("company") + if ( + selected_company + and self.filters.get("show_aggregate_value_from_subsidiary_companies") + and frappe.db.get_value("Company", selected_company, "is_group") + ): + lft, rgt = frappe.db.get_value("Company", selected_company, ["lft", "rgt"]) + child_companies = frappe.db.get_list( + "Company", filters={"lft": [">", lft], "rgt": ["<", rgt]}, pluck="name" + ) + + company_list.extend(child_companies) + + self.filters["company"] = company_list + def run(self): + self.update_company_list_for_parent_company() self.get_columns() self.get_data() self.get_chart_data() @@ -123,14 +144,23 @@ def get_sales_transactions_based_on_order_type(self): else: value_field = "total_qty" - self.entries = frappe.db.sql( - """ select s.order_type as entity, s.{value_field} as value_field, s.{date_field} - from `tab{doctype}` s where s.docstatus = 1 and s.company = %s and s.{date_field} between %s and %s - and ifnull(s.order_type, '') != '' order by s.order_type - """.format(date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type), - (self.filters.company, self.filters.from_date, self.filters.to_date), - as_dict=1, - ) + doctype = DocType(self.filters.doc_type) + + self.entries = ( + frappe.qb.from_(doctype) + .select( + doctype.order_type.as_("entity"), + doctype[self.date_field], + doctype[value_field].as_("value_field"), + ) + .where( + (doctype.docstatus == 1) + & (doctype.company.isin(self.filters.company)) + & (doctype[self.date_field].between(self.filters.from_date, self.filters.to_date)) + & (IfNull(doctype.order_type, "") != "") + ) + .orderby(doctype.order_type) + ).run(as_dict=True) self.get_teams() @@ -152,7 +182,7 @@ def get_sales_transactions_based_on_customers_or_suppliers(self): fields=[entity, entity_name, value_field, self.date_field], filters={ "docstatus": 1, - "company": self.filters.company, + "company": ["in", self.filters.company], self.date_field: ("between", [self.filters.from_date, self.filters.to_date]), }, ) @@ -167,16 +197,26 @@ def get_sales_transactions_based_on_items(self): else: value_field = "stock_qty" - self.entries = frappe.db.sql( - """ - select i.item_code as entity, i.item_name as entity_name, i.stock_uom, i.{value_field} as value_field, s.{date_field} - from `tab{doctype} Item` i , `tab{doctype}` s - where s.name = i.parent and i.docstatus = 1 and s.company = %s - and s.{date_field} between %s and %s - """.format(date_field=self.date_field, value_field=value_field, doctype=self.filters.doc_type), - (self.filters.company, self.filters.from_date, self.filters.to_date), - as_dict=1, - ) + doctype = DocType(self.filters.doc_type) + doctype_item = DocType(f"{self.filters.doc_type} Item") + + self.entries = ( + frappe.qb.from_(doctype_item) + .join(doctype) + .on(doctype.name == doctype_item.parent) + .select( + doctype_item.item_code.as_("entity"), + doctype_item.item_name.as_("entity_name"), + doctype_item.stock_uom, + doctype_item[value_field].as_("value_field"), + doctype[self.date_field], + ) + .where( + (doctype_item.docstatus == 1) + & (doctype.company.isin(self.filters.company)) + & (doctype[self.date_field].between(self.filters.from_date, self.filters.to_date)) + ) + ).run(as_dict=True) self.entity_names = {} for d in self.entries: @@ -201,7 +241,7 @@ def get_sales_transactions_based_on_customer_or_territory_group(self): fields=[entity_field, value_field, self.date_field], filters={ "docstatus": 1, - "company": self.filters.company, + "company": ["in", self.filters.company], self.date_field: ("between", [self.filters.from_date, self.filters.to_date]), }, ) @@ -213,16 +253,24 @@ def get_sales_transactions_based_on_item_group(self): else: value_field = "qty" - self.entries = frappe.db.sql( - f""" - select i.item_group as entity, i.{value_field} as value_field, s.{self.date_field} - from `tab{self.filters.doc_type} Item` i , `tab{self.filters.doc_type}` s - where s.name = i.parent and i.docstatus = 1 and s.company = %s - and s.{self.date_field} between %s and %s - """, - (self.filters.company, self.filters.from_date, self.filters.to_date), - as_dict=1, - ) + doctype = DocType(self.filters.doc_type) + doctype_item = DocType(f"{self.filters.doc_type} Item") + + self.entries = ( + frappe.qb.from_(doctype_item) + .join(doctype) + .on(doctype.name == doctype_item.parent) + .select( + doctype_item.item_group.as_("entity"), + doctype_item[value_field].as_("value_field"), + doctype[self.date_field], + ) + .where( + (doctype_item.docstatus == 1) + & (doctype.company.isin(self.filters.company)) + & (doctype[self.date_field].between(self.filters.from_date, self.filters.to_date)) + ) + ).run(as_dict=True) self.get_groups() @@ -239,7 +287,7 @@ def get_sales_transactions_based_on_project(self): fields=[entity, value_field, self.date_field], filters={ "docstatus": 1, - "company": self.filters.company, + "company": ["in", self.filters.company], "project": ["!=", ""], self.date_field: ("between", [self.filters.from_date, self.filters.to_date]), }, @@ -312,7 +360,7 @@ def get_period(self, posting_date): str(((posting_date.month - 1) // 3) + 1), str(posting_date.year) ) else: - year = get_fiscal_year(posting_date, company=self.filters.company) + year = get_fiscal_year(posting_date, company=self.filters.company[0]) period = str(year[0]) return period 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 70b021a9cab1..8fcf29bd7a6c 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -206,7 +206,7 @@ def prepare_data(data, so_elapsed_time, filters): def prepare_chart_data(pending, completed): - labels = ["Amount to Bill", "Billed Amount"] + labels = [_("Amount to Bill"), _("Billed Amount")] return { "data": {"labels": labels, "datasets": [{"values": [pending, completed]}]}, diff --git a/erpnext/selling/report/sales_order_trends/sales_order_trends.js b/erpnext/selling/report/sales_order_trends/sales_order_trends.js index 28bd55049300..a44353cf54b9 100644 --- a/erpnext/selling/report/sales_order_trends/sales_order_trends.js +++ b/erpnext/selling/report/sales_order_trends/sales_order_trends.js @@ -2,3 +2,10 @@ // License: GNU General Public License v3. See license.txt frappe.query_reports["Sales Order Trends"] = $.extend({}, erpnext.sales_trends_filters); + +frappe.query_reports["Sales Order Trends"]["filters"].push({ + fieldname: "include_closed_orders", + label: __("Include Closed Orders"), + fieldtype: "Check", + default: 0, +}); diff --git a/erpnext/setup/default_success_action.py b/erpnext/setup/default_success_action.py index 2b9e75c32652..dba205481843 100644 --- a/erpnext/setup/default_success_action.py +++ b/erpnext/setup/default_success_action.py @@ -11,14 +11,17 @@ def get_message(doctype): - return _("{0} has been submitted successfully").format(_(doctype)) + # Properly format the string with translated doctype + return _("{0} has been submitted successfully").format(doctype) def get_first_success_message(doctype): + # Reuse the get_message function for consistency return get_message(doctype) def get_default_success_action(): + # Loop through each doctype in the list and return formatted actions return [ { "doctype": "Success Action", diff --git a/erpnext/setup/doctype/brand/brand.js b/erpnext/setup/doctype/brand/brand.js index 5d16d734e0fe..99b4ace6cd20 100644 --- a/erpnext/setup/doctype/brand/brand.js +++ b/erpnext/setup/doctype/brand/brand.js @@ -3,22 +3,14 @@ frappe.ui.form.on("Brand", { setup: (frm) => { - frm.fields_dict["brand_defaults"].grid.get_field("default_warehouse").get_query = function ( - doc, - cdt, - cdn - ) { + frm.set_query("default_warehouse", "brand_defaults", function (doc, cdt, cdn) { const row = locals[cdt][cdn]; return { filters: { company: row.company }, }; - }; + }); - frm.fields_dict["brand_defaults"].grid.get_field("default_discount_account").get_query = function ( - doc, - cdt, - cdn - ) { + frm.set_query("default_discount_account", "brand_defaults", function (doc, cdt, cdn) { const row = locals[cdt][cdn]; return { filters: { @@ -27,13 +19,9 @@ frappe.ui.form.on("Brand", { is_group: 0, }, }; - }; + }); - frm.fields_dict["brand_defaults"].grid.get_field("buying_cost_center").get_query = function ( - doc, - cdt, - cdn - ) { + frm.set_query("buying_cost_center", "brand_defaults", function (doc, cdt, cdn) { const row = locals[cdt][cdn]; return { filters: { @@ -41,25 +29,17 @@ frappe.ui.form.on("Brand", { company: row.company, }, }; - }; + }); - frm.fields_dict["brand_defaults"].grid.get_field("expense_account").get_query = function ( - doc, - cdt, - cdn - ) { + frm.set_query("expense_account", "brand_defaults", function (doc, cdt, cdn) { const row = locals[cdt][cdn]; return { query: "erpnext.controllers.queries.get_expense_account", filters: { company: row.company }, }; - }; + }); - frm.fields_dict["brand_defaults"].grid.get_field("default_provisional_account").get_query = function ( - doc, - cdt, - cdn - ) { + frm.set_query("default_provisional_account", "brand_defaults", function (doc, cdt, cdn) { const row = locals[cdt][cdn]; return { filters: { @@ -68,13 +48,9 @@ frappe.ui.form.on("Brand", { is_group: 0, }, }; - }; + }); - frm.fields_dict["brand_defaults"].grid.get_field("selling_cost_center").get_query = function ( - doc, - cdt, - cdn - ) { + frm.set_query("selling_cost_center", "brand_defaults", function (doc, cdt, cdn) { const row = locals[cdt][cdn]; return { filters: { @@ -82,18 +58,14 @@ frappe.ui.form.on("Brand", { company: row.company, }, }; - }; + }); - frm.fields_dict["brand_defaults"].grid.get_field("income_account").get_query = function ( - doc, - cdt, - cdn - ) { + frm.set_query("income_account", "brand_defaults", function (doc, cdt, cdn) { const row = locals[cdt][cdn]; return { query: "erpnext.controllers.queries.get_income_account", filters: { company: row.company }, }; - }; + }); }, }); diff --git a/erpnext/setup/doctype/brand/brand.json b/erpnext/setup/doctype/brand/brand.json index 45b4db81f1f9..8511d2215513 100644 --- a/erpnext/setup/doctype/brand/brand.json +++ b/erpnext/setup/doctype/brand/brand.json @@ -56,10 +56,11 @@ "idx": 1, "image_field": "image", "links": [], - "modified": "2021-03-01 15:57:30.005783", + "modified": "2024-08-20 14:10:21.377962", "modified_by": "Administrator", "module": "Setup", "name": "Brand", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -108,4 +109,4 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "ASC" -} \ No newline at end of file +} diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 939eaf257122..72d28a705ad1 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -20,7 +20,6 @@ frappe.ui.form.on("Company", { }, setup: function (frm) { frm.__rename_queue = "long"; - erpnext.company.setup_queries(frm); frm.set_query("parent_company", function () { return { @@ -81,6 +80,8 @@ frappe.ui.form.on("Company", { }, refresh: function (frm) { + erpnext.company.setup_queries(frm); + frm.toggle_display("address_html", !frm.is_new()); if (!frm.is_new()) { @@ -251,6 +252,7 @@ erpnext.company.setup_queries = function (frm) { ["default_expense_account", { root_type: "Expense" }], ["default_income_account", { root_type: "Income" }], ["round_off_account", { root_type: "Expense" }], + ["round_off_for_opening", { root_type: "Liability", account_type: "Round Off for Opening" }], ["write_off_account", { root_type: "Expense" }], ["default_deferred_expense_account", {}], ["default_deferred_revenue_account", {}], diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 284bd2b7f22c..271b440fbda6 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -48,23 +48,30 @@ "default_bank_account", "default_cash_account", "default_receivable_account", - "round_off_account", - "round_off_cost_center", + "default_payable_account", "write_off_account", - "exchange_gain_loss_account", - "unrealized_exchange_gain_loss_account", "unrealized_profit_loss_account", "column_break0", "allow_account_creation_against_child_company", - "default_payable_account", "default_expense_account", "default_income_account", - "default_deferred_revenue_account", - "default_deferred_expense_account", "default_discount_account", "payment_terms", "cost_center", "default_finance_book", + "exchange_gain__loss_section", + "exchange_gain_loss_account", + "column_break_sttp", + "unrealized_exchange_gain_loss_account", + "round_off_section", + "round_off_account", + "round_off_cost_center", + "column_break_jqfo", + "round_off_for_opening", + "deferred_accounting_section", + "default_deferred_revenue_account", + "column_break_dcdl", + "default_deferred_expense_account", "advance_payments_section", "book_advance_payments_in_separate_party_account", "reconcile_on_advance_payment_date", @@ -286,7 +293,7 @@ { "fieldname": "default_settings", "fieldtype": "Section Break", - "label": "Accounts Settings", + "label": "Default Accounts", "oldfieldtype": "Section Break" }, { @@ -801,6 +808,39 @@ "fieldtype": "Link", "label": "Default Operating Cost Account", "options": "Account" + }, + { + "fieldname": "round_off_for_opening", + "fieldtype": "Link", + "label": "Round Off for Opening", + "options": "Account" + }, + { + "fieldname": "exchange_gain__loss_section", + "fieldtype": "Section Break", + "label": "Exchange Gain / Loss" + }, + { + "fieldname": "round_off_section", + "fieldtype": "Section Break", + "label": "Round Off" + }, + { + "fieldname": "deferred_accounting_section", + "fieldtype": "Section Break", + "label": "Deferred Accounting" + }, + { + "fieldname": "column_break_sttp", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_jqfo", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_dcdl", + "fieldtype": "Column Break" } ], "icon": "fa fa-building", @@ -808,7 +848,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2024-07-24 18:17:56.413971", + "modified": "2024-12-02 15:37:32.723176", "modified_by": "Administrator", "module": "Setup", "name": "Company", diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index f79ea60f5c4d..d781288c8bd9 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -91,6 +91,7 @@ class Company(NestedSet): rgt: DF.Int round_off_account: DF.Link | None round_off_cost_center: DF.Link | None + round_off_for_opening: DF.Link | None sales_monthly_history: DF.SmallText | None series_for_depreciation_entry: DF.Data | None stock_adjustment_account: DF.Link | None @@ -203,7 +204,7 @@ def validate_advance_account_currency(self): ): frappe.throw( _("'{0}' should be in company currency {1}.").format( - frappe.bold("Default Advance Received Account"), frappe.bold(self.default_currency) + frappe.bold(_("Default Advance Received Account")), frappe.bold(self.default_currency) ) ) @@ -214,7 +215,7 @@ def validate_advance_account_currency(self): ): frappe.throw( _("'{0}' should be in company currency {1}.").format( - frappe.bold("Default Advance Paid Account"), frappe.bold(self.default_currency) + frappe.bold(_("Default Advance Paid Account")), frappe.bold(self.default_currency) ) ) @@ -447,7 +448,7 @@ def validate_provisional_account_for_non_stock_items(self): ): frappe.throw( _("Set default {0} account for non stock items").format( - frappe.bold("Provisional Account") + frappe.bold(_("Provisional Account")) ) ) diff --git a/erpnext/setup/doctype/currency_exchange/currency_exchange.js b/erpnext/setup/doctype/currency_exchange/currency_exchange.js index 82f0e22ee61b..d4501e5d0daf 100644 --- a/erpnext/setup/doctype/currency_exchange/currency_exchange.js +++ b/erpnext/setup/doctype/currency_exchange/currency_exchange.js @@ -1,30 +1,32 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -extend_cscript(cur_frm.cscript, { - onload: function () { - if (cur_frm.doc.__islocal) { - cur_frm.set_value("to_currency", frappe.defaults.get_global_default("currency")); +frappe.ui.form.on("Currency Exchange", { + onload: function (frm) { + if (frm.doc.__islocal) { + frm.set_value("to_currency", frappe.defaults.get_global_default("currency")); } }, - refresh: function () { - cur_frm.cscript.set_exchange_rate_label(); + refresh: function (frm) { + // Don't trigger on Quick Entry form + if (typeof frm.is_dialog === "undefined") { + frm.trigger("set_exchange_rate_label"); + } }, - from_currency: function () { - cur_frm.cscript.set_exchange_rate_label(); + from_currency: function (frm) { + frm.trigger("set_exchange_rate_label"); }, - to_currency: function () { - cur_frm.cscript.set_exchange_rate_label(); + to_currency: function (frm) { + frm.trigger("set_exchange_rate_label"); }, - - set_exchange_rate_label: function () { - if (cur_frm.doc.from_currency && cur_frm.doc.to_currency) { - var default_label = __(frappe.meta.docfield_map[cur_frm.doctype]["exchange_rate"].label); - cur_frm.fields_dict.exchange_rate.set_label( - default_label + repl(" (1 %(from_currency)s = [?] %(to_currency)s)", cur_frm.doc) + set_exchange_rate_label: function (frm) { + if (frm.doc.from_currency && frm.doc.to_currency) { + var default_label = __(frappe.meta.docfield_map[frm.doctype]["exchange_rate"].label); + frm.fields_dict.exchange_rate.set_label( + default_label + repl(" (1 %(from_currency)s = [?] %(to_currency)s)", frm.doc) ); } }, diff --git a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py index 3249c93ef87c..d28b1e65b7bc 100644 --- a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py +++ b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py @@ -68,9 +68,9 @@ def json(self): if kwargs["params"].get("date") and kwargs["params"].get("from") and kwargs["params"].get("to"): if test_exchange_values.get(kwargs["params"]["date"]): return PatchResponse({"result": test_exchange_values[kwargs["params"]["date"]]}, 200) - elif args[0].startswith("https://frankfurter.app") and kwargs.get("params"): + elif args[0].startswith("https://api.frankfurter.app") and kwargs.get("params"): if kwargs["params"].get("base") and kwargs["params"].get("symbols"): - date = args[0].replace("https://frankfurter.app/", "") + date = args[0].replace("https://api.frankfurter.app/", "") if test_exchange_values.get(date): return PatchResponse( {"rates": {kwargs["params"].get("symbols"): test_exchange_values.get(date)}}, 200 diff --git a/erpnext/setup/doctype/customer_group/customer_group.py b/erpnext/setup/doctype/customer_group/customer_group.py index 06f2f43374e1..5dd0fd020119 100644 --- a/erpnext/setup/doctype/customer_group/customer_group.py +++ b/erpnext/setup/doctype/customer_group/customer_group.py @@ -71,14 +71,9 @@ def validate_currency_for_receivable_and_advance_account(self): ) def on_update(self): - self.validate_name_with_customer() super().on_update() self.validate_one_root() - def validate_name_with_customer(self): - if frappe.db.exists("Customer", self.name): - frappe.msgprint(_("A customer with the same name already exists"), raise_exception=1) - def get_parent_customer_groups(customer_group): lft, rgt = frappe.db.get_value("Customer Group", customer_group, ["lft", "rgt"]) diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.py b/erpnext/setup/doctype/holiday_list/holiday_list.py index 0216f75a6286..b7920236ce1c 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/holiday_list.py @@ -149,7 +149,11 @@ def validate_duplicate_date(self): unique_dates = [] for row in self.holidays: if row.holiday_date in unique_dates: - frappe.throw(_("Holiday Date {0} added multiple times").format(frappe.bold(row.holiday_date))) + frappe.throw( + _("Holiday Date {0} added multiple times").format( + frappe.bold(formatdate(row.holiday_date)) + ) + ) unique_dates.append(row.holiday_date) diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py index c9c3c837cebe..ce3f918f7eb6 100644 --- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py +++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py @@ -485,8 +485,14 @@ def is_deletion_doc_running(company: str | None = None, err_msg: str | None = No def check_for_running_deletion_job(doc, method=None): # Check if DocType has 'company' field - df = qb.DocType("DocField") - if qb.from_(df).select(df.parent).where((df.fieldname == "company") & (df.parent == doc.doctype)).run(): - is_deletion_doc_running( - doc.company, _("Cannot make any transactions until the deletion job is completed") - ) + if doc.doctype not in ("GL Entry", "Payment Ledger Entry", "Stock Ledger Entry"): + df = qb.DocType("DocField") + if ( + qb.from_(df) + .select(df.parent) + .where((df.fieldname == "company") & (df.parent == doc.doctype)) + .run() + ): + is_deletion_doc_running( + doc.company, _("Cannot make any transactions until the deletion job is completed") + ) diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index bba9e79a348d..97ec418d9557 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -98,7 +98,7 @@ def setup_currency_exchange(): ces.set("result_key", []) ces.set("req_params", []) - ces.api_endpoint = "https://frankfurter.app/{transaction_date}" + ces.api_endpoint = "https://api.frankfurter.app/{transaction_date}" ces.append("result_key", {"key": "rates"}) ces.append("result_key", {"key": "{to_currency}"}) ces.append("req_params", {"key": "base", "value": "{from_currency}"}) diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index a746ebee7ed4..efe1d705c51c 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -449,18 +449,355 @@ }, "France": { - "France VAT 20%": { - "account_name": "VAT 20%", - "tax_rate": 20, - "default": 1 - }, - "France VAT 10%": { - "account_name": "VAT 10%", - "tax_rate": 10 - }, - "France VAT 5.5%": { - "account_name": "VAT 5.5%", - "tax_rate": 5.5 + "chart_of_accounts": { + "France - Plan Comptable General avec code": { + "sales_tax_templates": [ + { + "title": "TVA 20% Collectée", + "tax_category": "Vente Domestique", + "taxes": [ + { + "account_head": { + "account_name": "TVA 20% Collectée", + "account_number": "445720", + "root_type": "Liability", + "tax_rate": 20.0 + }, + "description": "TVA 20%", + "rate": 20 + } + ] + }, + { + "title": "TVA 10% Collectée", + "tax_category": "Vente Domestique", + "taxes": [ + { + "account_head": { + "account_name": "TVA 10% Collectée", + "account_number": "445710", + "root_type": "Liability", + "tax_rate": 10.0 + }, + "description": "TVA 10%", + "rate": 10 + } + ] + }, + { + "title": "TVA 5.5% Collectée", + "tax_category": "Vente Domestique", + "taxes": [ + { + "account_head": { + "account_name": "TVA 5.5% Collectée", + "account_number": "445755", + "root_type": "Liability", + "tax_rate": 5.5 + }, + "description": "TVA 5.5%", + "rate": 5.5 + } + ] + }, + { + "title": "TVA 2.1% Collectée", + "tax_category": "Vente Domestique", + "taxes": [ + { + "account_head": { + "account_name": "TVA 2.1% Collectée", + "account_number": "445721", + "root_type": "Liability", + "tax_rate": 2.10 + }, + "description": "TVA 2.1%", + "rate": 2.1 + } + ] + } + ], + "purchase_tax_templates": [ + { + "title": "TVA 20% Déductible", + "tax_category": "Achat Domestique", + "taxes": [ + { + "account_head": { + "account_name": "TVA 20% Déductible", + "account_number": "445620", + "root_type": "Asset", + "tax_rate": 20.0 + }, + "description": "TVA 20%", + "rate": 20 + } + ] + }, + { + "title": "TVA 10% Déductible", + "tax_category": "Achat Domestique", + "taxes": [ + { + "account_head": { + "account_name": "TVA 10% Déductible", + "account_number": "445610", + "root_type": "Asset", + "tax_rate": 10.0 + }, + "description": "TVA 10%", + "rate": 10 + } + ] + }, + { + "title": "TVA 5.5% Déductible", + "tax_category": "Achat Domestique", + "taxes": [ + { + "account_head": { + "account_name": "TVA 5.5% Déductible", + "account_number": "445655", + "root_type": "Asset", + "tax_rate": 5.5 + }, + "description": "TVA 5.5%", + "rate": 5.5 + } + ] + }, + { + "title": "TVA 2.1% Déductible", + "tax_category": "Achat Domestique", + "taxes": [ + { + "account_head": { + "account_name": "TVA 2.1% Déductible", + "account_number": "445621", + "root_type": "Asset", + "tax_rate": 2.1 + }, + "description": "TVA 2.1%", + "rate": 2.1 + } + ] + }, + { + "title": "TVA 20% Déductible - Incluse dans le prix", + "taxes": [ + { + "account_head": { + "account_name": "TVA 20% Déductible", + "account_number": "445620", + "root_type": "Asset", + "tax_rate": 20.0 + }, + "included_in_print_rate": 1, + "description": "TVA 20%", + "rate": 20 + } + ] + }, + { + "title": "TVA 10% Déductible - Incluse dans le prix", + "taxes": [ + { + "account_head": { + "account_name": "TVA 10% Déductible", + "account_number": "445610", + "root_type": "Asset", + "tax_rate": 10.0 + }, + "included_in_print_rate": 1, + "description": "TVA 10%", + "rate": 10 + } + ] + }, + { + "title": "TVA 5.5% Déductible - Incluse dans le prix", + "taxes": [ + { + "account_head": { + "account_name": "TVA 5.5% Déductible", + "account_number": "445655", + "root_type": "Asset", + "tax_rate": 5.5 + }, + "included_in_print_rate": 1, + "description": "TVA 5.5%", + "rate": 5.5 + } + ] + }, + { + "title": "TVA 2.1% Déductible - Incluse dans le prix", + "taxes": [ + { + "account_head": { + "account_name": "TVA 2.1% Déductible", + "account_number": "445621", + "root_type": "Asset", + "tax_rate": 2.1 + }, + "included_in_print_rate": 1, + "description": "TVA 2.1%", + "rate": 2.1 + } + ] + }, + { + "title": "TVA Intracommunautaire", + "tax_category": "Achat - EU", + "taxes": [ + { + "account_head": { + "account_name": "TVA déductible sur acquisition intracommunautaires", + "account_number": "445662", + "root_type": "Asset", + "tax_rate": 20.0, + "add_deduct_tax": "Add" + }, + "description": "TVA déductible sur acquisition intracommunautaires", + "rate": 20 + }, + { + "account_head": { + "account_name": "TVA due intracommunautaire", + "account_number": "445200", + "root_type": "Asset", + "tax_rate": 20.0, + "add_deduct_tax": "Deduct" + }, + "description": "TVA due intracommunautaire", + "rate": 20 + } + ] + } + ], + "item_tax_templates": [ + { + "title": "TVA 20% Déductible - Achat", + "taxes": [ + { + "tax_type": { + "account_name": "TVA 20% Déductible", + "account_number": "445620", + "root_type": "Asset", + "tax_rate": 20.0 + }, + "description": "TVA 20%", + "tax_rate": 20 + } + ] + }, + { + "title": "TVA 10% Déductible - Achat", + "taxes": [ + { + "tax_type": { + "account_name": "TVA 10% Déductible", + "account_number": "445610", + "root_type": "Asset", + "tax_rate": 10.0 + }, + "description": "TVA 10%", + "tax_rate": 10 + } + ] + }, + { + "title": "TVA 5.5% Déductible - Achat", + "taxes": [ + { + "tax_type": { + "account_name": "TVA 5.5% Déductible", + "account_number": "445655", + "root_type": "Asset", + "tax_rate": 5.5 + }, + "description": "TVA 5.5%", + "tax_rate": 5.5 + } + ] + }, + { + "title": "TVA 2.1% Déductible - Achat", + "taxes": [ + { + "tax_type": { + "account_name": "TVA 2.1% Déductible", + "account_number": "445621", + "root_type": "Asset", + "tax_rate": 2.1 + }, + "description": "TVA 2.1%", + "tax_rate": 2.1 + } + ] + }, + { + "title": "TVA 20% Collecté - Vente", + "taxes": [ + { + "tax_type": { + "account_name": "TVA 20% Collecté", + "account_number": "445720", + "root_type": "Liability", + "tax_rate": 20.0 + }, + "description": "TVA 20%", + "tax_rate": 20 + } + ] + }, + { + "title": "TVA 10% Collecté - Vente", + "taxes": [ + { + "tax_type": { + "account_name": "TVA 10% Collecté", + "account_number": "445710", + "root_type": "Liability", + "tax_rate": 10.0 + }, + "description": "TVA 10%", + "tax_rate": 10 + } + ] + }, + { + "title": "TVA 5.5% Collecté - Vente", + "taxes": [ + { + "tax_type": { + "account_name": "TVA 5.5% Collecté", + "account_number": "445755", + "root_type": "Liability", + "tax_rate": 5.5 + }, + "description": "TVA 5.5%", + "tax_rate": 5.5 + } + ] + }, + { + "title": "TVA 2.1% Collecté - Vente", + "taxes": [ + { + "tax_type": { + "account_name": "TVA 2.1% Collecté", + "account_number": "445721", + "root_type": "Liability", + "tax_rate": 2.1 + }, + "description": "TVA 2.1%", + "tax_rate": 2.1 + } + ] + } + ] + } } }, diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 0bcb9fb60194..270a9e060543 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -66,29 +66,54 @@ def install(country=None): "parent_item_group": _("All Item Groups"), }, # Stock Entry Type - {"doctype": "Stock Entry Type", "name": "Material Issue", "purpose": "Material Issue"}, - {"doctype": "Stock Entry Type", "name": "Material Receipt", "purpose": "Material Receipt"}, + { + "doctype": "Stock Entry Type", + "name": "Material Issue", + "purpose": "Material Issue", + "is_standard": 1, + }, + { + "doctype": "Stock Entry Type", + "name": "Material Receipt", + "purpose": "Material Receipt", + "is_standard": 1, + }, { "doctype": "Stock Entry Type", "name": "Material Transfer", "purpose": "Material Transfer", + "is_standard": 1, + }, + { + "doctype": "Stock Entry Type", + "name": "Manufacture", + "purpose": "Manufacture", + "is_standard": 1, + }, + { + "doctype": "Stock Entry Type", + "name": "Repack", + "purpose": "Repack", + "is_standard": 1, }, - {"doctype": "Stock Entry Type", "name": "Manufacture", "purpose": "Manufacture"}, - {"doctype": "Stock Entry Type", "name": "Repack", "purpose": "Repack"}, + {"doctype": "Stock Entry Type", "name": "Disassemble", "purpose": "Disassemble", "is_standard": 1}, { "doctype": "Stock Entry Type", "name": "Send to Subcontractor", "purpose": "Send to Subcontractor", + "is_standard": 1, }, { "doctype": "Stock Entry Type", "name": "Material Transfer for Manufacture", "purpose": "Material Transfer for Manufacture", + "is_standard": 1, }, { "doctype": "Stock Entry Type", "name": "Material Consumption for Manufacture", "purpose": "Material Consumption for Manufacture", + "is_standard": 1, }, # territory: with two default territories, one for home country and one named Rest of the World { diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index 384673448b09..6561f386c555 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -86,7 +86,13 @@ def simple_to_detailed(templates): def from_detailed_data(company_name, data): """Create Taxes and Charges Templates from detailed data.""" - coa_name = frappe.db.get_value("Company", company_name, "chart_of_accounts") + charts_company_name = company_name + if ( + frappe.db.get_value("Company", company_name, "create_chart_of_accounts_based_on") + == "Existing Company" + ): + charts_company_name = frappe.db.get_value("Company", company_name, "existing_company") + coa_name = frappe.db.get_value("Company", charts_company_name, "chart_of_accounts") coa_data = data.get("chart_of_accounts", {}) tax_templates = coa_data.get(coa_name) or coa_data.get("*", {}) tax_categories = data.get("tax_categories") diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js index 36a51056bb45..6fc9e6666a25 100644 --- a/erpnext/stock/dashboard/item_dashboard.js +++ b/erpnext/stock/dashboard/item_dashboard.js @@ -48,17 +48,18 @@ erpnext.stock.ItemDashboard = class ItemDashboard { let actual_qty = unescape(element.attr("data-actual_qty")); let disable_quick_entry = Number(unescape(element.attr("data-disable_quick_entry"))); let entry_type = action === "Move" ? "Material Transfer" : "Material Receipt"; + let stock_uom = unescape(element.attr("data-stock-uom")); if (disable_quick_entry) { open_stock_entry(item, warehouse, entry_type); } else { if (action === "Add") { let rate = unescape($(this).attr("data-rate")); - erpnext.stock.move_item(item, null, warehouse, actual_qty, rate, function () { + erpnext.stock.move_item(item, null, warehouse, actual_qty, rate, stock_uom, function () { me.refresh(); }); } else { - erpnext.stock.move_item(item, warehouse, null, actual_qty, null, function () { + erpnext.stock.move_item(item, warehouse, null, actual_qty, null, stock_uom, function () { me.refresh(); }); } @@ -207,7 +208,7 @@ erpnext.stock.ItemDashboard = class ItemDashboard { } }; -erpnext.stock.move_item = function (item, source, target, actual_qty, rate, callback) { +erpnext.stock.move_item = function (item, source, target, actual_qty, rate, stock_uom, callback) { var dialog = new frappe.ui.Dialog({ title: target ? __("Add Item") : __("Move Item"), fields: [ @@ -295,6 +296,8 @@ erpnext.stock.move_item = function (item, source, target, actual_qty, rate, call let row = frappe.model.add_child(doc, "items"); row.item_code = dialog.get_value("item_code"); row.s_warehouse = dialog.get_value("source"); + row.stock_uom = stock_uom; + row.uom = stock_uom; row.t_warehouse = dialog.get_value("target"); row.qty = dialog.get_value("qty"); row.conversion_factor = 1; diff --git a/erpnext/stock/dashboard/item_dashboard.py b/erpnext/stock/dashboard/item_dashboard.py index e6382688669b..3d7c21639e0f 100644 --- a/erpnext/stock/dashboard/item_dashboard.py +++ b/erpnext/stock/dashboard/item_dashboard.py @@ -71,6 +71,7 @@ def get_data( item.update( { "item_name": frappe.get_cached_value("Item", item.item_code, "item_name"), + "stock_uom": frappe.get_cached_value("Item", item.item_code, "stock_uom"), "disable_quick_entry": frappe.get_cached_value("Item", item.item_code, "has_batch_no") or frappe.get_cached_value("Item", item.item_code, "has_serial_no"), "projected_qty": flt(item.projected_qty, precision), diff --git a/erpnext/stock/dashboard/item_dashboard_list.html b/erpnext/stock/dashboard/item_dashboard_list.html index 3b2619133bf4..ae90ff80686a 100644 --- a/erpnext/stock/dashboard/item_dashboard_list.html +++ b/erpnext/stock/dashboard/item_dashboard_list.html @@ -49,12 +49,14 @@ data-disable_quick_entry="{{ d.disable_quick_entry }}" data-warehouse="{{ d.warehouse }}" data-actual_qty="{{ d.actual_qty }}" + data-stock-uom="{{ d.stock_uom }}" data-item="{{ escape(d.item_code) }}">{{ __("Move") }} {% endif %}
diff --git a/erpnext/stock/dashboard_chart/oldest_items/oldest_items.json b/erpnext/stock/dashboard_chart/oldest_items/oldest_items.json index 9c10a5346be9..a55fe7a6a6c9 100644 --- a/erpnext/stock/dashboard_chart/oldest_items/oldest_items.json +++ b/erpnext/stock/dashboard_chart/oldest_items/oldest_items.json @@ -1,22 +1,23 @@ { "chart_name": "Oldest Items", "chart_type": "Report", - "creation": "2020-07-20 21:01:04.336845", + "creation": "2022-03-30 00:58:02.041721", "custom_options": "{\"colors\": [\"#5e64ff\"]}", "docstatus": 0, "doctype": "Dashboard Chart", - "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"to_date\":\"frappe.datetime.nowdate()\"}", - "filters_json": "{\"range1\":30,\"range2\":60,\"range3\":90,\"show_warehouse_wise_stock\":0}", + "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"to_date\":\"frappe.datetime.nowdate()\",\"range1\":\"30\",\"range2\":\"60\",\"range3\":\"90\"}", + "filters_json": "{\"range\":\"30, 60, 90\",\"show_warehouse_wise_stock\":0}", "idx": 0, "is_public": 1, "is_standard": 1, - "modified": "2020-07-29 14:50:26.846482", + "modified": "2024-09-23 22:37:51.392999", "modified_by": "Administrator", "module": "Stock", "name": "Oldest Items", "number_of_groups": 0, "owner": "Administrator", "report_name": "Stock Ageing", + "roles": [], "timeseries": 0, "type": "Bar", "use_report_chart": 1, diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index c539c3157473..5d71ca06cb4b 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -188,9 +188,9 @@ def set_expiry_date(self): if has_expiry_date and not self.expiry_date: frappe.throw( msg=_("Please set {0} for Batched Item {1}, which is used to set {2} on Submit.").format( - frappe.bold("Shelf Life in Days"), + frappe.bold(_("Shelf Life in Days")), get_link_to_form("Item", self.item), - frappe.bold("Batch Expiry Date"), + frappe.bold(_("Batch Expiry Date")), ), title=_("Expiry Date Mandatory"), ) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 87c333370b24..4a0580f0e94a 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -108,8 +108,9 @@ "dispatch_address", "company_address_section", "company_address", - "column_break_101", "company_address_display", + "column_break_101", + "company_contact_person", "terms_tab", "tc_name", "terms", @@ -1391,13 +1392,20 @@ "fieldname": "named_place", "fieldtype": "Data", "label": "Named Place" + }, + { + "fieldname": "company_contact_person", + "fieldtype": "Link", + "label": "Company Contact Person", + "options": "Contact", + "print_hide": 1 } ], "icon": "fa fa-truck", "idx": 146, "is_submittable": 1, "links": [], - "modified": "2024-03-20 16:05:02.854990", + "modified": "2024-11-26 12:44:28.258215", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 8a096aca80c9..2b4dad137c25 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -53,6 +53,7 @@ class DeliveryNote(SellingController): company: DF.Link company_address: DF.Link | None company_address_display: DF.SmallText | None + company_contact_person: DF.Link | None contact_display: DF.SmallText | None contact_email: DF.Data | None contact_mobile: DF.SmallText | None @@ -208,6 +209,8 @@ def __init__(self, *args, **kwargs): ) def onload(self): + super().onload() + if self.docstatus == 0: self.set_onload("has_unpacked_items", self.has_unpacked_items()) @@ -360,52 +363,33 @@ def validate_references(self): self.validate_sales_invoice_references() def validate_sales_order_references(self): - err_msg = "" - for item in self.items: - if (item.against_sales_order and not item.so_detail) or ( - not item.against_sales_order and item.so_detail - ): - if not item.against_sales_order: - err_msg += ( - _("'Sales Order' reference ({1}) is missing in row {0}").format( - frappe.bold(item.idx), frappe.bold("against_sales_order") - ) - + "
" - ) - else: - err_msg += ( - _("'Sales Order Item' reference ({1}) is missing in row {0}").format( - frappe.bold(item.idx), frappe.bold("so_detail") - ) - + "
" - ) - - if err_msg: - frappe.throw(err_msg, title=_("References to Sales Orders are Incomplete")) + self._validate_dependent_item_fields( + "against_sales_order", "so_detail", _("References to Sales Orders are Incomplete") + ) def validate_sales_invoice_references(self): - err_msg = "" + self._validate_dependent_item_fields( + "against_sales_invoice", "si_detail", _("References to Sales Invoices are Incomplete") + ) + + def _validate_dependent_item_fields(self, field_a: str, field_b: str, error_title: str): + errors = [] for item in self.items: - if (item.against_sales_invoice and not item.si_detail) or ( - not item.against_sales_invoice and item.si_detail - ): - if not item.against_sales_invoice: - err_msg += ( - _("'Sales Invoice' reference ({1}) is missing in row {0}").format( - frappe.bold(item.idx), frappe.bold("against_sales_invoice") - ) - + "
" - ) - else: - err_msg += ( - _("'Sales Invoice Item' reference ({1}) is missing in row {0}").format( - frappe.bold(item.idx), frappe.bold("si_detail") - ) - + "
" + missing_label = None + if item.get(field_a) and not item.get(field_b): + missing_label = item.meta.get_label(field_b) + elif item.get(field_b) and not item.get(field_a): + missing_label = item.meta.get_label(field_a) + + if missing_label and missing_label != "No Label": + errors.append( + _("The field {0} in row {1} is not set").format( + frappe.bold(_(missing_label)), frappe.bold(item.idx) ) + ) - if err_msg: - frappe.throw(err_msg, title=_("References to Sales Invoices are Incomplete")) + if errors: + frappe.throw("
".join(errors), title=error_title) def validate_proj_cust(self): """check for does customer belong to same project as entered..""" @@ -1037,7 +1021,6 @@ def get_pending_qty(item_row): "parent": "delivery_note", "so_detail": "so_detail", "against_sales_order": "sales_order", - "serial_no": "serial_no", "cost_center": "cost_center", }, "postprocess": update_item, @@ -1047,7 +1030,7 @@ def get_pending_qty(item_row): }, "Sales Taxes and Charges": { "doctype": "Sales Taxes and Charges", - "add_if_empty": True, + "reset_value": not (args and args.get("merge_taxes")), "ignore": args.get("merge_taxes") if args else 0, }, "Sales Team": { @@ -1063,7 +1046,7 @@ def get_pending_qty(item_row): automatically_fetch_payment_terms = cint( frappe.db.get_single_value("Accounts Settings", "automatically_fetch_payment_terms") ) - if automatically_fetch_payment_terms: + if automatically_fetch_payment_terms and not doc.is_return: doc.set_payment_schedule() return doc @@ -1205,18 +1188,19 @@ def postprocess(source, target): # As we are using session user details in the pickup_contact then pickup_contact_person will be session user target.pickup_contact_person = frappe.session.user - contact = frappe.db.get_value( - "Contact", source.contact_person, ["email_id", "phone", "mobile_no"], as_dict=1 - ) - delivery_contact_display = f"{source.contact_display}" - if contact: - if contact.email_id: - delivery_contact_display += "
" + contact.email_id - if contact.phone: - delivery_contact_display += "
" + contact.phone - if contact.mobile_no and not contact.phone: - delivery_contact_display += "
" + contact.mobile_no - target.delivery_contact = delivery_contact_display + if source.contact_person: + contact = frappe.db.get_value( + "Contact", source.contact_person, ["email_id", "phone", "mobile_no"], as_dict=1 + ) + delivery_contact_display = f"{source.contact_display}" + if contact: + if contact.email_id: + delivery_contact_display += "
" + contact.email_id + if contact.phone: + delivery_contact_display += "
" + contact.phone + if contact.mobile_no and not contact.phone: + delivery_contact_display += "
" + contact.mobile_no + target.delivery_contact = delivery_contact_display if source.shipping_address_name: target.delivery_address_name = source.shipping_address_name diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index cfe550f2f275..9acdce8bebc4 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -3,6 +3,7 @@ import json +from collections import defaultdict import frappe from frappe.tests.utils import FrappeTestCase @@ -2039,6 +2040,305 @@ def test_warranty_expiry_date_for_serial_item(self): self.assertEqual(sn.status, "Delivered") self.assertEqual(sn.warranty_period, 100) + def test_batch_return_dn(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + item_code = make_item( + "Test Batch Return DN Item 1", + properties={ + "has_batch_no": 1, + "valuation_method": "Moving Average", + "create_new_batch": 1, + "batch_number_series": "TBRDN1-.#####", + "is_stock_item": 1, + }, + ).name + + se = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=5, basic_rate=100) + + batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + dn = create_delivery_note( + item_code=item_code, + qty=5, + rate=500, + use_serial_batch_fields=1, + batch_no=batch_no, + ) + + dn_return = make_sales_return(dn.name) + dn_return.save().submit() + + self.assertEqual(dn_return.items[0].qty, 5 * -1) + + returned_batch_no = get_batch_from_bundle(dn_return.items[0].serial_and_batch_bundle) + self.assertEqual(batch_no, returned_batch_no) + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": dn_return.name, "voucher_type": "Delivery Note"}, + "stock_value_difference", + ) + + self.assertEqual(stock_value_difference, 100.0 * 5) + + def test_delivery_note_return_valuation_without_use_serial_batch_field(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + batch_item = make_item( + "_Test Delivery Note Return Valuation Batch Item", + properties={ + "has_batch_no": 1, + "create_new_batch": 1, + "is_stock_item": 1, + "batch_number_series": "BRTN-DNN-BI-.#####", + }, + ).name + + serial_item = make_item( + "_Test Delivery Note Return Valuation Serial Item", + properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "SRTN-DNN-TP-.#####"}, + ).name + + batches = {} + serial_nos = [] + for qty, rate in {3: 300, 2: 100}.items(): + se = make_stock_entry( + item_code=batch_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate + ) + batches[get_batch_from_bundle(se.items[0].serial_and_batch_bundle)] = qty + + for qty, rate in {2: 100, 1: 50}.items(): + make_stock_entry(item_code=serial_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate) + serial_nos.extend(get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)) + + dn = create_delivery_note( + item_code=batch_item, + qty=5, + rate=1000, + use_serial_batch_fields=0, + batches=batches, + do_not_submit=True, + ) + + bundle_id = make_serial_batch_bundle( + frappe._dict( + { + "item_code": serial_item, + "warehouse": dn.items[0].warehouse, + "qty": 3, + "voucher_type": "Delivery Note", + "serial_nos": serial_nos, + "posting_date": dn.posting_date, + "posting_time": dn.posting_time, + "type_of_transaction": "Outward", + "do_not_submit": True, + } + ) + ).name + + dn.append( + "items", + { + "item_code": serial_item, + "qty": 3, + "rate": 700, + "base_rate": 700, + "item_name": serial_item, + "uom": "Nos", + "stock_uom": "Nos", + "conversion_factor": 1, + "warehouse": dn.items[0].warehouse, + "use_serial_batch_fields": 0, + "serial_and_batch_bundle": bundle_id, + }, + ) + + dn.save() + dn.submit() + dn.reload() + + batch_no_valuation = defaultdict(float) + serial_no_valuation = defaultdict(float) + + for row in dn.items: + if row.serial_and_batch_bundle: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + fields=["incoming_rate", "serial_no", "batch_no"], + ) + + for d in bundle_data: + if d.batch_no: + batch_no_valuation[d.batch_no] = d.incoming_rate + elif d.serial_no: + serial_no_valuation[d.serial_no] = d.incoming_rate + + return_entry = make_sales_return(dn.name) + + return_entry.save() + return_entry.submit() + return_entry.reload() + + for row in return_entry.items: + if row.item_code == batch_item: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + fields=["incoming_rate", "batch_no"], + ) + + for d in bundle_data: + self.assertEqual(d.incoming_rate, batch_no_valuation[d.batch_no]) + else: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + fields=["incoming_rate", "serial_no"], + ) + + for d in bundle_data: + self.assertEqual(d.incoming_rate, serial_no_valuation[d.serial_no]) + + def test_delivery_note_return_valuation_with_use_serial_batch_field(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + batch_item = make_item( + "_Test Delivery Note Return Valuation WITH Batch Item", + properties={ + "has_batch_no": 1, + "create_new_batch": 1, + "is_stock_item": 1, + "batch_number_series": "BRTN-DNN-BIW-.#####", + }, + ).name + + serial_item = make_item( + "_Test Delivery Note Return Valuation WITH Serial Item", + properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "SRTN-DNN-TPW-.#####"}, + ).name + + batches = [] + serial_nos = [] + for qty, rate in {3: 300, 2: 100}.items(): + se = make_stock_entry( + item_code=batch_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate + ) + batches.append(get_batch_from_bundle(se.items[0].serial_and_batch_bundle)) + + for qty, rate in {2: 100, 1: 50}.items(): + se = make_stock_entry( + item_code=serial_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate + ) + serial_nos.extend(get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)) + + dn = create_delivery_note( + item_code=batch_item, + qty=3, + rate=1000, + use_serial_batch_fields=1, + batch_no=batches[0], + do_not_submit=True, + ) + + dn.append( + "items", + { + "item_code": batch_item, + "qty": 2, + "rate": 1000, + "base_rate": 1000, + "item_name": batch_item, + "uom": dn.items[0].uom, + "stock_uom": dn.items[0].uom, + "conversion_factor": 1, + "warehouse": dn.items[0].warehouse, + "use_serial_batch_fields": 1, + "batch_no": batches[1], + }, + ) + + dn.append( + "items", + { + "item_code": serial_item, + "qty": 2, + "rate": 700, + "base_rate": 700, + "item_name": serial_item, + "uom": "Nos", + "stock_uom": "Nos", + "conversion_factor": 1, + "warehouse": dn.items[0].warehouse, + "use_serial_batch_fields": 1, + "serial_no": "\n".join(serial_nos[0:2]), + }, + ) + + dn.append( + "items", + { + "item_code": serial_item, + "qty": 1, + "rate": 700, + "base_rate": 700, + "item_name": serial_item, + "uom": "Nos", + "stock_uom": "Nos", + "conversion_factor": 1, + "warehouse": dn.items[0].warehouse, + "use_serial_batch_fields": 1, + "serial_no": serial_nos[-1], + }, + ) + + dn.save() + dn.submit() + dn.reload() + + batch_no_valuation = defaultdict(float) + serial_no_valuation = defaultdict(float) + + for row in dn.items: + if row.serial_and_batch_bundle: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + fields=["incoming_rate", "serial_no", "batch_no"], + ) + + for d in bundle_data: + if d.batch_no: + batch_no_valuation[d.batch_no] = d.incoming_rate + elif d.serial_no: + serial_no_valuation[d.serial_no] = d.incoming_rate + + return_entry = make_sales_return(dn.name) + + return_entry.save() + return_entry.submit() + return_entry.reload() + + for row in return_entry.items: + if row.item_code == batch_item: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + fields=["incoming_rate", "batch_no"], + ) + + for d in bundle_data: + self.assertEqual(d.incoming_rate, batch_no_valuation[d.batch_no]) + else: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + fields=["incoming_rate", "serial_no"], + ) + + for d in bundle_data: + self.assertEqual(d.incoming_rate, serial_no_valuation[d.serial_no]) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") @@ -2066,6 +2366,9 @@ def create_delivery_note(**args): if args.get("batch_no"): batches = frappe._dict({args.batch_no: qty}) + if args.get("batches"): + batches = frappe._dict(args.batches) + bundle_id = make_serial_batch_bundle( frappe._dict( { diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index b8164b25753f..56e5209da59c 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -87,16 +87,19 @@ "column_break_rxvc", "batch_no", "available_qty_section", - "actual_batch_qty", "actual_qty", - "installed_qty", - "item_tax_rate", + "actual_batch_qty", "column_break_atna", + "company_total_stock", + "section_break_kejd", + "installed_qty", "packed_qty", + "column_break_fguf", "received_qty", "accounting_details_section", "expense_account", "column_break_71", + "item_tax_rate", "internal_transfer_section", "material_request", "purchase_order", @@ -519,7 +522,7 @@ "allow_on_submit": 1, "fieldname": "actual_qty", "fieldtype": "Float", - "label": "Available Qty at From Warehouse", + "label": "Qty (Warehouse)", "no_copy": 1, "oldfieldname": "actual_qty", "oldfieldtype": "Currency", @@ -907,13 +910,30 @@ { "fieldname": "column_break_rxvc", "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "fieldname": "company_total_stock", + "fieldtype": "Float", + "label": "Qty (Company)", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "section_break_kejd", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_fguf", + "fieldtype": "Column Break" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-03-21 18:15:07.603672", + "modified": "2024-11-21 17:37:37.441498", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", @@ -923,4 +943,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py index b76f74297283..716cd7d4856a 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py @@ -30,6 +30,7 @@ class DeliveryNoteItem(Document): batch_no: DF.Link | None billed_amt: DF.Currency brand: DF.Link | None + company_total_stock: DF.Float conversion_factor: DF.Float cost_center: DF.Link | None customer_item_code: DF.Data | None diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py index 3bafa12983f9..661605bdf5f0 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py @@ -107,6 +107,7 @@ def delete_custom_fields(self): self.source_fieldname, f"to_{self.source_fieldname}", f"from_{self.source_fieldname}", + f"rejected_{self.source_fieldname}", ], ) } @@ -171,12 +172,12 @@ def get_dimension_fields(self, doctype=None): if label_start_with: label = f"{label_start_with} {self.dimension_name}" - return [ + dimension_fields = [ dict( fieldname="inventory_dimension", fieldtype="Section Break", insert_after=self.get_insert_after_fieldname(doctype), - label="Inventory Dimension", + label=_("Inventory Dimension"), collapsible=1, ), dict( @@ -184,26 +185,39 @@ def get_dimension_fields(self, doctype=None): fieldtype="Link", insert_after="inventory_dimension", options=self.reference_document, - label=label, + label=_(label), search_index=1, reqd=self.reqd, mandatory_depends_on=self.mandatory_depends_on, ), ] + if doctype in ["Purchase Invoice Item", "Purchase Receipt Item"]: + dimension_fields.append( + dict( + fieldname="rejected_" + self.source_fieldname, + fieldtype="Link", + insert_after=self.source_fieldname, + options=self.reference_document, + label=_("Rejected " + self.dimension_name), + search_index=1, + reqd=self.reqd, + mandatory_depends_on=self.mandatory_depends_on, + ) + ) + + return dimension_fields + def add_custom_fields(self): custom_fields = {} dimension_fields = [] if self.apply_to_all_doctypes: for doctype in get_inventory_documents(): - if field_exists(doctype[0], self.source_fieldname): - continue - dimension_fields = self.get_dimension_fields(doctype[0]) self.add_transfer_field(doctype[0], dimension_fields) custom_fields.setdefault(doctype[0], dimension_fields) - elif not field_exists(self.document_type, self.source_fieldname): + else: dimension_fields = self.get_dimension_fields() self.add_transfer_field(self.document_type, dimension_fields) @@ -222,8 +236,17 @@ def add_custom_fields(self): dimension_field["fieldname"] = self.target_fieldname custom_fields["Stock Ledger Entry"] = dimension_field + filter_custom_fields = {} if custom_fields: - create_custom_fields(custom_fields) + for doctype, fields in custom_fields.items(): + if isinstance(fields, dict): + fields = [fields] + + for field in fields: + if not field_exists(doctype, field["fieldname"]): + filter_custom_fields.setdefault(doctype, []).append(field) + + create_custom_fields(filter_custom_fields) def add_transfer_field(self, doctype, dimension_fields): if doctype not in [ diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index 8ce954d55e69..f8128ce0033b 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -16,6 +16,7 @@ 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.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import InventoryDimensionNegativeStockError from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse @@ -268,21 +269,47 @@ def test_for_purchase_sales_and_stock_transaction(self): item_code = "Test Inventory Dimension Item" create_item(item_code) warehouse = create_warehouse("Store Warehouse") + rj_warehouse = create_warehouse("RJ Warehouse") + + if not frappe.db.exists("Store", "Rejected Store"): + frappe.get_doc({"doctype": "Store", "store_name": "Rejected Store"}).insert( + ignore_permissions=True + ) # Purchase Receipt -> Inward in Store 1 pr_doc = make_purchase_receipt( - item_code=item_code, warehouse=warehouse, qty=10, rate=100, do_not_submit=True + item_code=item_code, + warehouse=warehouse, + qty=10, + rejected_qty=5, + rate=100, + rejected_warehouse=rj_warehouse, + do_not_submit=True, ) pr_doc.items[0].store = "Store 1" + pr_doc.items[0].rejected_store = "Rejected Store" pr_doc.save() pr_doc.submit() - entries = get_voucher_sl_entries(pr_doc.name, ["warehouse", "store", "incoming_rate"]) + entries = frappe.get_all( + "Stock Ledger Entry", + filters={"voucher_no": pr_doc.name, "warehouse": warehouse}, + fields=["store"], + order_by="creation", + ) - self.assertEqual(entries[0].warehouse, warehouse) self.assertEqual(entries[0].store, "Store 1") + entries = frappe.get_all( + "Stock Ledger Entry", + filters={"voucher_no": pr_doc.name, "warehouse": rj_warehouse}, + fields=["store"], + order_by="creation", + ) + + self.assertEqual(entries[0].store, "Rejected Store") + # Stock Entry -> Transfer from Store 1 to Store 2 se_doc = make_stock_entry( item_code=item_code, qty=10, from_warehouse=warehouse, to_warehouse=warehouse, do_not_save=True @@ -426,39 +453,49 @@ def test_validate_negative_stock_for_inventory_dimension(self): warehouse = create_warehouse("Negative Stock Warehouse") + # Try issuing 10 qty, more than available stock against inventory dimension doc = make_stock_entry(item_code=item_code, source=warehouse, qty=10, do_not_submit=True) doc.items[0].inv_site = "Site 1" - self.assertRaises(frappe.ValidationError, doc.submit) + self.assertRaises(InventoryDimensionNegativeStockError, doc.submit) + + # cancel the stock entry doc.reload() if doc.docstatus == 1: doc.cancel() + # Receive 10 qty against inventory dimension doc = make_stock_entry(item_code=item_code, target=warehouse, qty=10, do_not_submit=True) - doc.items[0].to_inv_site = "Site 1" doc.submit() + # check inventory dimension value in stock ledger entry site_name = frappe.get_all( "Stock Ledger Entry", filters={"voucher_no": doc.name, "is_cancelled": 0}, fields=["inv_site"] )[0].inv_site self.assertEqual(site_name, "Site 1") - doc = make_stock_entry(item_code=item_code, source=warehouse, qty=100, do_not_submit=True) + # Receive another 100 qty without inventory dimension + doc = make_stock_entry(item_code=item_code, target=warehouse, qty=100) + # Try issuing 100 qty, more than available stock against inventory dimension + # Note: total available qty for the item is 110, but against inventory dimension, only 10 qty is available + doc = make_stock_entry(item_code=item_code, source=warehouse, qty=100, do_not_submit=True) doc.items[0].inv_site = "Site 1" - self.assertRaises(frappe.ValidationError, doc.submit) + self.assertRaises(InventoryDimensionNegativeStockError, doc.submit) + # disable validate_negative_stock for inventory dimension inv_dimension.reload() inv_dimension.db_set("validate_negative_stock", 0) frappe.local.inventory_dimensions = {} + # Try issuing 100 qty, more than available stock against inventory dimension doc = make_stock_entry(item_code=item_code, source=warehouse, qty=100, do_not_submit=True) - doc.items[0].inv_site = "Site 1" doc.submit() self.assertEqual(doc.docstatus, 1) + # check inventory dimension value in stock ledger entry site_name = frappe.get_all( "Stock Ledger Entry", filters={"voucher_no": doc.name, "is_cancelled": 0}, fields=["inv_site"] )[0].inv_site diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 81eeb914af08..4195506ad3b9 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -663,39 +663,41 @@ $.extend(erpnext.item, { } frm.doc.attributes.forEach(function (d) { - let p = new Promise((resolve) => { - if (!d.numeric_values) { - frappe - .call({ - method: "frappe.client.get_list", - args: { - doctype: "Item Attribute Value", - filters: [["parent", "=", d.attribute]], - fields: ["attribute_value"], - limit_page_length: 0, - parent: "Item Attribute", - order_by: "idx", - }, - }) - .then((r) => { - if (r.message) { - attr_val_fields[d.attribute] = r.message.map(function (d) { - return d.attribute_value; - }); - resolve(); - } - }); - } else { - let values = []; - for (var i = d.from_range; i <= d.to_range; i = flt(i + d.increment, 6)) { - values.push(i); + if (!d.disabled) { + let p = new Promise((resolve) => { + if (!d.numeric_values) { + frappe + .call({ + method: "frappe.client.get_list", + args: { + doctype: "Item Attribute Value", + filters: [["parent", "=", d.attribute]], + fields: ["attribute_value"], + limit_page_length: 0, + parent: "Item Attribute", + order_by: "idx", + }, + }) + .then((r) => { + if (r.message) { + attr_val_fields[d.attribute] = r.message.map(function (d) { + return d.attribute_value; + }); + resolve(); + } + }); + } else { + let values = []; + for (var i = d.from_range; i <= d.to_range; i = flt(i + d.increment, 6)) { + values.push(i); + } + attr_val_fields[d.attribute] = values; + resolve(); } - attr_val_fields[d.attribute] = values; - resolve(); - } - }); + }); - promises.push(p); + promises.push(p); + } }, this); Promise.all(promises).then(() => { @@ -710,26 +712,29 @@ $.extend(erpnext.item, { for (var i = 0; i < frm.doc.attributes.length; i++) { var fieldtype, desc; var row = frm.doc.attributes[i]; - if (row.numeric_values) { - fieldtype = "Float"; - desc = - "Min Value: " + - row.from_range + - " , Max Value: " + - row.to_range + - ", in Increments of: " + - row.increment; - } else { - fieldtype = "Data"; - desc = ""; + + if (!row.disabled) { + if (row.numeric_values) { + fieldtype = "Float"; + desc = + "Min Value: " + + row.from_range + + " , Max Value: " + + row.to_range + + ", in Increments of: " + + row.increment; + } else { + fieldtype = "Data"; + desc = ""; + } + fields = fields.concat({ + label: row.attribute, + fieldname: row.attribute, + fieldtype: fieldtype, + reqd: 0, + description: desc, + }); } - fields = fields.concat({ - label: row.attribute, - fieldname: row.attribute, - fieldtype: fieldtype, - reqd: 0, - description: desc, - }); } if (frm.doc.image) { diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 1ceb949d6910..e40b3822af6b 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -22,6 +22,7 @@ strip_html, ) from frappe.utils.html_utils import clean_html +from pypika import Order import erpnext from erpnext.controllers.item_variant import ( @@ -345,7 +346,13 @@ def add_default_uom_in_conversion_factor_table(self): def validate_item_tax_net_rate_range(self): for tax in self.get("taxes"): if flt(tax.maximum_net_rate) < flt(tax.minimum_net_rate): - frappe.throw(_("Row #{0}: Maximum Net Rate cannot be greater than Minimum Net Rate")) + frappe.throw( + _("Taxes row #{0}: {1} cannot be smaller than {2}").format( + tax.idx, + bold(_(tax.meta.get_label("maximum_net_rate"))), + bold(_(tax.meta.get_label("minimum_net_rate"))), + ) + ) def update_template_tables(self): template = frappe.get_cached_doc("Item", self.variant_of) @@ -1133,34 +1140,10 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): """returns last purchase details in stock uom""" # get last purchase order item details - last_purchase_order = frappe.db.sql( - """\ - select po.name, po.transaction_date, po.conversion_rate, - po_item.conversion_factor, po_item.base_price_list_rate, - po_item.discount_percentage, po_item.base_rate, po_item.base_net_rate - from `tabPurchase Order` po, `tabPurchase Order Item` po_item - where po.docstatus = 1 and po_item.item_code = %s and po.name != %s and - po.name = po_item.parent - order by po.transaction_date desc, po.name desc - limit 1""", - (item_code, cstr(doc_name)), - as_dict=1, - ) + last_purchase_order = get_purchase_voucher_details("Purchase Order", item_code, doc_name) # get last purchase receipt item details - last_purchase_receipt = frappe.db.sql( - """\ - select pr.name, pr.posting_date, pr.posting_time, pr.conversion_rate, - pr_item.conversion_factor, pr_item.base_price_list_rate, pr_item.discount_percentage, - pr_item.base_rate, pr_item.base_net_rate - from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pr_item - where pr.docstatus = 1 and pr_item.item_code = %s and pr.name != %s and - pr.name = pr_item.parent - order by pr.posting_date desc, pr.posting_time desc, pr.name desc - limit 1""", - (item_code, cstr(doc_name)), - as_dict=1, - ) + last_purchase_receipt = get_purchase_voucher_details("Purchase Receipt", item_code, doc_name) purchase_order_date = getdate( last_purchase_order and last_purchase_order[0].transaction_date or "1900-01-01" @@ -1181,7 +1164,13 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): purchase_date = purchase_receipt_date else: - return frappe._dict() + last_purchase_invoice = get_purchase_voucher_details("Purchase Invoice", item_code, doc_name) + + if last_purchase_invoice: + last_purchase = last_purchase_invoice[0] + purchase_date = getdate(last_purchase.posting_date) + else: + return frappe._dict() conversion_factor = flt(last_purchase.conversion_factor) out = frappe._dict( @@ -1207,6 +1196,40 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): return out +def get_purchase_voucher_details(doctype, item_code, document_name): + parent_doc = frappe.qb.DocType(doctype) + child_doc = frappe.qb.DocType(doctype + " Item") + + query = ( + frappe.qb.from_(parent_doc) + .inner_join(child_doc) + .on(parent_doc.name == child_doc.parent) + .select( + parent_doc.name, + parent_doc.conversion_rate, + child_doc.conversion_factor, + child_doc.base_price_list_rate, + child_doc.discount_percentage, + child_doc.base_rate, + child_doc.base_net_rate, + ) + .where(parent_doc.docstatus == 1) + .where(child_doc.item_code == item_code) + .where(parent_doc.name != document_name) + ) + + if doctype in ("Purchase Receipt", "Purchase Invoice"): + query = query.select(parent_doc.posting_date, parent_doc.posting_time) + query = query.orderby( + parent_doc.posting_date, parent_doc.posting_time, parent_doc.name, order=Order.desc + ) + else: + query = query.select(parent_doc.transaction_date) + query = query.orderby(parent_doc.transaction_date, parent_doc.name, order=Order.desc) + + return query.run(as_dict=1) + + def check_stock_uom_with_bin(item, stock_uom): if stock_uom == frappe.db.get_value("Item", item, "stock_uom"): return diff --git a/erpnext/stock/doctype/item/templates/item_row.html b/erpnext/stock/doctype/item/templates/item_row.html index f81fc1d8743a..809ac838ccb0 100644 --- a/erpnext/stock/doctype/item/templates/item_row.html +++ b/erpnext/stock/doctype/item/templates/item_row.html @@ -1,4 +1,4 @@ diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.json b/erpnext/stock/doctype/item_attribute/item_attribute.json index 5c4678916f33..d9b0898ca7f5 100644 --- a/erpnext/stock/doctype/item_attribute/item_attribute.json +++ b/erpnext/stock/doctype/item_attribute/item_attribute.json @@ -10,6 +10,8 @@ "field_order": [ "attribute_name", "numeric_values", + "column_break_vbik", + "disabled", "section_break_4", "from_range", "increment", @@ -70,15 +72,26 @@ "fieldtype": "Table", "label": "Item Attribute Values", "options": "Item Attribute Value" + }, + { + "fieldname": "column_break_vbik", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" } ], "icon": "fa fa-edit", "index_web_pages_for_search": 1, "links": [], - "modified": "2020-10-02 12:03:02.359202", + "modified": "2024-11-26 20:05:29.421714", "modified_by": "Administrator", "module": "Stock", "name": "Item Attribute", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -94,4 +107,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/item_attribute/item_attribute.py b/erpnext/stock/doctype/item_attribute/item_attribute.py index 04421d6292eb..3b9bcf93288e 100644 --- a/erpnext/stock/doctype/item_attribute/item_attribute.py +++ b/erpnext/stock/doctype/item_attribute/item_attribute.py @@ -30,6 +30,7 @@ class ItemAttribute(Document): from erpnext.stock.doctype.item_attribute_value.item_attribute_value import ItemAttributeValue attribute_name: DF.Data + disabled: DF.Check from_range: DF.Float increment: DF.Float item_attribute_values: DF.Table[ItemAttributeValue] @@ -47,6 +48,19 @@ def validate(self): def on_update(self): self.validate_exising_items() + self.set_enabled_disabled_in_items() + + def set_enabled_disabled_in_items(self): + db_value = self.get_doc_before_save() + if not db_value or db_value.disabled != self.disabled: + item_variant_table = frappe.qb.DocType("Item Variant Attribute") + query = ( + frappe.qb.update(item_variant_table) + .set(item_variant_table.disabled, self.disabled) + .where(item_variant_table.attribute == self.name) + ) + + query.run() def validate_exising_items(self): """Validate that if there are existing items with attributes, they are valid""" diff --git a/erpnext/stock/doctype/item_variant_attribute/item_variant_attribute.json b/erpnext/stock/doctype/item_variant_attribute/item_variant_attribute.json index 9699ecbb3db2..cfc752c1f61c 100644 --- a/erpnext/stock/doctype/item_variant_attribute/item_variant_attribute.json +++ b/erpnext/stock/doctype/item_variant_attribute/item_variant_attribute.json @@ -11,6 +11,7 @@ "column_break_2", "attribute_value", "numeric_values", + "disabled", "section_break_4", "from_range", "increment", @@ -74,11 +75,18 @@ "fieldname": "to_range", "fieldtype": "Float", "label": "To Range" + }, + { + "default": "0", + "fetch_from": "attribute.disabled", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" } ], "istable": 1, "links": [], - "modified": "2023-07-14 17:15:19.112119", + "modified": "2024-11-26 20:10:49.873339", "modified_by": "Administrator", "module": "Stock", "name": "Item Variant Attribute", @@ -87,4 +95,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/item_variant_attribute/item_variant_attribute.py b/erpnext/stock/doctype/item_variant_attribute/item_variant_attribute.py index 756b7421761d..ea239d2ccc96 100644 --- a/erpnext/stock/doctype/item_variant_attribute/item_variant_attribute.py +++ b/erpnext/stock/doctype/item_variant_attribute/item_variant_attribute.py @@ -16,6 +16,7 @@ class ItemVariantAttribute(Document): attribute: DF.Link attribute_value: DF.Data | None + disabled: DF.Check from_range: DF.Float increment: DF.Float numeric_values: DF.Check diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index f04acc85ed54..09c01a0ee883 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -107,6 +107,14 @@ frappe.ui.form.on("Material Request", { if (flt(frm.doc.per_received, precision) < 100) { frm.add_custom_button(__("Stop"), () => frm.events.update_status(frm, "Stopped")); + + if (frm.doc.material_request_type === "Purchase") { + frm.add_custom_button( + __("Purchase Order"), + () => frm.events.make_purchase_order(frm), + __("Create") + ); + } } if (flt(frm.doc.per_ordered, precision) < 100) { @@ -149,14 +157,6 @@ frappe.ui.form.on("Material Request", { ); } - if (frm.doc.material_request_type === "Purchase") { - frm.add_custom_button( - __("Purchase Order"), - () => frm.events.make_purchase_order(frm), - __("Create") - ); - } - if (frm.doc.material_request_type === "Purchase") { frm.add_custom_button( __("Request for Quotation"), @@ -259,18 +259,21 @@ frappe.ui.form.on("Material Request", { }, callback: function (r) { const d = item; - const allow_to_change_fields = [ + let allow_to_change_fields = [ "actual_qty", "projected_qty", "min_order_qty", "item_name", - "description", "stock_uom", "uom", "conversion_factor", "stock_qty", ]; + if (overwrite_warehouse) { + allow_to_change_fields.push("description"); + } + if (!r.exc) { $.each(r.message, function (key, value) { if (!d[key] || allow_to_change_fields.includes(key)) { diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 117ed2614391..23d289170dba 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -267,6 +267,7 @@ def update_completed_qty(self, mr_items=None, update_modified=True): mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance") for d in self.get("items"): + precision = d.precision("ordered_qty") if d.name in mr_items: if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"): d.ordered_qty = flt(mr_items_ordered_qty.get(d.name)) @@ -276,14 +277,14 @@ def update_completed_qty(self, mr_items=None, update_modified=True): (d.qty + (d.qty * (mr_qty_allowance / 100))), d.precision("ordered_qty") ) - if d.ordered_qty and d.ordered_qty > allowed_qty: + if d.ordered_qty and flt(d.ordered_qty, precision) > flt(allowed_qty, precision): frappe.throw( _( "The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than allowed requested quantity {2} for Item {3}" ).format(d.ordered_qty, d.parent, allowed_qty, d.item_code) ) - elif d.ordered_qty and d.ordered_qty > d.stock_qty: + elif d.ordered_qty and flt(d.ordered_qty, precision) > flt(d.stock_qty, precision): frappe.throw( _( "The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than requested quantity {2} for Item {3}" @@ -377,7 +378,9 @@ def set_missing_values(source, target_doc): def update_item(obj, target, source_parent): target.conversion_factor = obj.conversion_factor - target.qty = flt(flt(obj.stock_qty) - flt(obj.ordered_qty)) / target.conversion_factor + + qty = obj.received_qty or obj.ordered_qty + target.qty = flt(flt(obj.stock_qty) - flt(qty)) / target.conversion_factor target.stock_qty = target.qty * target.conversion_factor if getdate(target.schedule_date) < getdate(nowdate()): target.schedule_date = None @@ -429,7 +432,9 @@ def select_item(d): filtered_items = args.get("filtered_children", []) child_filter = d.name in filtered_items if filtered_items else True - return d.ordered_qty < d.stock_qty and child_filter + qty = d.received_qty or d.ordered_qty + + return qty < d.stock_qty and child_filter doclist = get_mapped_doc( "Material Request", diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 0c93fd6c402b..ba1cc228bfa7 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -109,7 +109,7 @@ def validate_stock_qty(self): "actual_qty", ) - if row.qty > bin_qty: + if row.qty > flt(bin_qty): frappe.throw( _( "At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} in the warehouse {4}." @@ -650,6 +650,8 @@ def _get_pick_list_items(self, items): if self.name: query = query.where(pi_item.parent != self.name) + query = query.for_update() + return query.run(as_dict=True) def _get_product_bundles(self) -> dict[str, str]: diff --git a/erpnext/stock/doctype/pick_list/pick_list_list.js b/erpnext/stock/doctype/pick_list/pick_list_list.js index 9cdbfe187205..eca6eece7855 100644 --- a/erpnext/stock/doctype/pick_list/pick_list_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list_list.js @@ -4,7 +4,7 @@ frappe.listview_settings["Pick List"] = { get_indicator: function (doc) { const status_colors = { - Draft: "grey", + Draft: "red", Open: "orange", Completed: "green", Cancelled: "red", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index bfac4381a060..bcecf8be14de 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -11,25 +11,10 @@ erpnext.buying.setup_buying_controller(); frappe.ui.form.on("Purchase Receipt", { setup: (frm) => { - frm.make_methods = { - "Landed Cost Voucher": () => { - let lcv = frappe.model.get_new_doc("Landed Cost Voucher"); - lcv.company = frm.doc.company; - - let lcv_receipt = frappe.model.get_new_doc("Landed Cost Purchase Receipt"); - lcv_receipt.receipt_document_type = "Purchase Receipt"; - lcv_receipt.receipt_document = frm.doc.name; - lcv_receipt.supplier = frm.doc.supplier; - lcv_receipt.grand_total = frm.doc.grand_total; - lcv.purchase_receipts = [lcv_receipt]; - - frappe.set_route("Form", lcv.doctype, lcv.name); - }, - }; - frm.custom_make_buttons = { "Stock Entry": "Return", "Purchase Invoice": "Purchase Invoice", + "Landed Cost Voucher": "Landed Cost Voucher", }; frm.set_query("expense_account", "items", function () { @@ -114,9 +99,35 @@ frappe.ui.form.on("Purchase Receipt", { } } + if (frm.doc.docstatus === 1) { + frm.add_custom_button( + __("Landed Cost Voucher"), + () => { + frm.events.make_lcv(frm); + }, + __("Create") + ); + } + frm.events.add_custom_buttons(frm); }, + make_lcv(frm) { + frappe.call({ + method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_lcv", + args: { + doctype: frm.doc.doctype, + docname: frm.doc.name, + }, + callback: (r) => { + if (r.message) { + var doc = frappe.model.sync(r.message); + frappe.set_route("Form", doc[0].doctype, doc[0].name); + } + }, + }); + }, + add_custom_buttons: function (frm) { if (frm.doc.docstatus == 0) { frm.add_custom_button( diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 61a180caba45..643f9e7a82f1 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -889,7 +889,7 @@ "no_copy": 1, "oldfieldname": "status", "oldfieldtype": "Select", - "options": "\nDraft\nTo Bill\nCompleted\nReturn Issued\nCancelled\nClosed", + "options": "\nDraft\nPartly Billed\nTo Bill\nCompleted\nReturn Issued\nCancelled\nClosed", "print_hide": 1, "print_width": "150px", "read_only": 1, @@ -1273,7 +1273,7 @@ "idx": 261, "is_submittable": 1, "links": [], - "modified": "2024-07-04 14:50:10.538472", + "modified": "2024-11-13 16:55:14.129055", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 3c97d13b904a..76ecf0fd596a 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -112,7 +112,9 @@ class PurchaseReceipt(BuyingController): shipping_address: DF.Link | None shipping_address_display: DF.SmallText | None shipping_rule: DF.Link | None - status: DF.Literal["", "Draft", "To Bill", "Completed", "Return Issued", "Cancelled", "Closed"] + status: DF.Literal[ + "", "Draft", "Partly Billed", "To Bill", "Completed", "Return Issued", "Cancelled", "Closed" + ] subcontracting_receipt: DF.Link | None supplied_items: DF.Table[PurchaseReceiptItemSupplied] supplier: DF.Link @@ -837,7 +839,11 @@ def make_tax_gl_entries(self, gl_entries, via_landed_cost_voucher=False): def update_assets(self, item, valuation_rate): assets = frappe.db.get_all( "Asset", - filters={"purchase_receipt": self.name, "item_code": item.item_code}, + filters={ + "purchase_receipt": self.name, + "item_code": item.item_code, + "purchase_receipt_item": ("in", [item.name, ""]), + }, fields=["name", "asset_quantity"], ) @@ -1055,6 +1061,8 @@ def get_billed_amount_against_po(po_items): def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate=False): # Update Billing % based on pending accepted qty + buying_settings = frappe.get_single("Buying Settings") + total_amount, total_billed_amount = 0, 0 item_wise_returned_qty = get_item_wise_returned_qty(pr_doc) @@ -1062,19 +1070,25 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate returned_qty = flt(item_wise_returned_qty.get(item.name)) returned_amount = flt(returned_qty) * flt(item.rate) pending_amount = flt(item.amount) - returned_amount - total_billable_amount = pending_amount if item.billed_amt <= pending_amount else item.billed_amt + if buying_settings.bill_for_rejected_quantity_in_purchase_invoice: + pending_amount = flt(item.amount) + + total_billable_amount = abs(flt(item.amount)) + if pending_amount > 0: + total_billable_amount = pending_amount if item.billed_amt <= pending_amount else item.billed_amt total_amount += total_billable_amount - total_billed_amount += flt(item.billed_amt) + total_billed_amount += abs(flt(item.billed_amt)) if pr_doc.get("is_return") and not total_amount and total_billed_amount: total_amount = total_billed_amount if adjust_incoming_rate: adjusted_amt = 0.0 - if item.billed_amt and item.amount: + if item.billed_amt is not None and item.amount is not None: adjusted_amt = flt(item.billed_amt) - flt(item.amount) + adjusted_amt = adjusted_amt * flt(pr_doc.conversion_rate) item.db_set("rate_difference_with_purchase_invoice", adjusted_amt, update_modified=False) percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6) @@ -1209,7 +1223,7 @@ def get_pending_qty(item_row): }, "Purchase Taxes and Charges": { "doctype": "Purchase Taxes and Charges", - "add_if_empty": True, + "reset_value": not (args and args.get("merge_taxes")), "ignore": args.get("merge_taxes") if args else 0, }, }, @@ -1365,3 +1379,26 @@ def get_item_account_wise_additional_cost(purchase_document): @erpnext.allow_regional def update_regional_gl_entries(gl_list, doc): return + + +@frappe.whitelist() +def make_lcv(doctype, docname): + landed_cost_voucher = frappe.new_doc("Landed Cost Voucher") + + details = frappe.db.get_value(doctype, docname, ["supplier", "company", "base_grand_total"], as_dict=1) + + landed_cost_voucher.company = details.company + + landed_cost_voucher.append( + "purchase_receipts", + { + "receipt_document_type": doctype, + "receipt_document": docname, + "grand_total": details.base_grand_total, + "supplier": details.supplier, + }, + ) + + landed_cost_voucher.get_items_from_purchase_receipts() + + return landed_cost_voucher.as_dict() diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index c239360d945b..86d1a6948dee 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -3,7 +3,7 @@ import frappe from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import add_days, cint, cstr, flt, getdate, nowtime, today +from frappe.utils import add_days, cint, cstr, flt, get_datetime, getdate, nowtime, today from pypika import functions as fn import erpnext @@ -3592,6 +3592,398 @@ def test_internal_transfer_for_batch_items_with_cancel_use_serial_batch_fields(s inter_transfer_dn.cancel() frappe.db.set_single_value("Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 1) + def test_sles_with_same_posting_datetime_and_creation(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + from erpnext.stock.report.stock_balance.stock_balance import execute + + item_code = "Test Item for SLE with same posting datetime and creation" + create_item(item_code) + + pr = make_purchase_receipt( + item_code=item_code, + qty=10, + rate=100, + posting_date="2023-11-06", + posting_time="00:00:00", + ) + + sr = make_stock_entry( + item_code=item_code, + source=pr.items[0].warehouse, + qty=10, + posting_date="2023-11-07", + posting_time="14:28:0.330404", + ) + + sle = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": sr.doctype, "voucher_no": sr.name, "item_code": sr.items[0].item_code}, + "name", + ) + + sle_doc = frappe.get_doc("Stock Ledger Entry", sle) + sle_doc.db_set("creation", "2023-11-07 14:28:01.208930") + + sle_doc.reload() + self.assertEqual(get_datetime(sle_doc.creation), get_datetime("2023-11-07 14:28:01.208930")) + + sr = make_stock_entry( + item_code=item_code, + target=pr.items[0].warehouse, + qty=50, + posting_date="2023-11-07", + posting_time="14:28:0.920825", + ) + + sle = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": sr.doctype, "voucher_no": sr.name, "item_code": sr.items[0].item_code}, + "name", + ) + + sle_doc = frappe.get_doc("Stock Ledger Entry", sle) + sle_doc.db_set("creation", "2023-11-07 14:28:01.044561") + + sle_doc.reload() + self.assertEqual(get_datetime(sle_doc.creation), get_datetime("2023-11-07 14:28:01.044561")) + + pr.repost_future_sle_and_gle(force=True) + + columns, data = execute( + filters=frappe._dict( + {"item_code": item_code, "warehouse": pr.items[0].warehouse, "company": pr.company} + ) + ) + + self.assertEqual(data[0].get("bal_qty"), 50.0) + + def test_same_stock_and_transaction_uom_conversion_factor(self): + item_code = "Test Item for Same Stock and Transaction UOM Conversion Factor" + create_item(item_code) + + pr = make_purchase_receipt( + item_code=item_code, + qty=10, + rate=100, + stock_uom="Nos", + transaction_uom="Nos", + conversion_factor=10, + ) + + self.assertEqual(pr.items[0].conversion_factor, 1.0) + + def test_purchase_receipt_return_valuation_without_use_serial_batch_field(self): + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return + + batch_item = make_item( + "_Test Purchase Receipt Return Valuation Batch Item", + properties={ + "has_batch_no": 1, + "create_new_batch": 1, + "is_stock_item": 1, + "batch_number_series": "BRTN-TPRBI-.#####", + }, + ).name + + serial_item = make_item( + "_Test Purchase Receipt Return Valuation Serial Item", + properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "SRTN-TPRSI-.#####"}, + ).name + + rej_warehouse = create_warehouse("_Test Purchase Warehouse For Rejected Qty") + + pr = make_purchase_receipt( + item_code=batch_item, + received_qty=10, + qty=8, + rejected_qty=2, + rejected_warehouse=rej_warehouse, + rate=300, + do_not_submit=1, + use_serial_batch_fields=0, + ) + + pr.append( + "items", + { + "item_code": serial_item, + "qty": 2, + "rate": 100, + "base_rate": 100, + "item_name": serial_item, + "uom": "Nos", + "stock_uom": "Nos", + "conversion_factor": 1, + "rejected_qty": 1, + "warehouse": pr.items[0].warehouse, + "use_serial_batch_fields": 0, + "rejected_warehouse": rej_warehouse, + }, + ) + + pr.save() + pr.submit() + pr.reload() + + batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle) + rejected_batch_no = get_batch_from_bundle(pr.items[0].rejected_serial_and_batch_bundle) + + self.assertEqual(batch_no, rejected_batch_no) + + return_entry = make_purchase_return(pr.name) + + return_entry.save() + return_entry.submit() + return_entry.reload() + + for row in return_entry.items: + if row.item_code == batch_item: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 300.00) + else: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 100.00) + + for row in return_entry.items: + if row.item_code == batch_item: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.rejected_serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 0) + else: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.rejected_serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 0) + + def test_purchase_receipt_return_valuation_with_use_serial_batch_field(self): + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return + + batch_item = make_item( + "_Test Purchase Receipt Return Valuation With Batch Item", + properties={"has_batch_no": 1, "create_new_batch": 1, "is_stock_item": 1}, + ).name + + serial_item = make_item( + "_Test Purchase Receipt Return Valuation With Serial Item", + properties={"has_serial_no": 1, "is_stock_item": 1}, + ).name + + rej_warehouse = create_warehouse("_Test Purchase Warehouse For Rejected Qty") + + batch_no = "BATCH-RTN-BNU-TPRBI-0001" + serial_nos = ["SNU-RTN-TPRSI-0001", "SNU-RTN-TPRSI-0002", "SNU-RTN-TPRSI-0003"] + + if not frappe.db.exists("Batch", batch_no): + frappe.get_doc( + { + "doctype": "Batch", + "batch_id": batch_no, + "item": batch_item, + } + ).insert() + + for serial_no in serial_nos: + if not frappe.db.exists("Serial No", serial_no): + frappe.get_doc( + { + "doctype": "Serial No", + "item_code": serial_item, + "serial_no": serial_no, + } + ).insert() + + pr = make_purchase_receipt( + item_code=batch_item, + received_qty=10, + qty=8, + rejected_qty=2, + rejected_warehouse=rej_warehouse, + batch_no=batch_no, + use_serial_batch_fields=1, + rate=300, + do_not_submit=1, + ) + + pr.append( + "items", + { + "item_code": serial_item, + "qty": 2, + "rate": 100, + "base_rate": 100, + "item_name": serial_item, + "uom": "Nos", + "stock_uom": "Nos", + "conversion_factor": 1, + "rejected_qty": 1, + "warehouse": pr.items[0].warehouse, + "use_serial_batch_fields": 1, + "rejected_warehouse": rej_warehouse, + "serial_no": "\n".join(serial_nos[:2]), + "rejected_serial_no": serial_nos[2], + }, + ) + + pr.save() + pr.submit() + pr.reload() + + batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle) + rejected_batch_no = get_batch_from_bundle(pr.items[0].rejected_serial_and_batch_bundle) + + self.assertEqual(batch_no, rejected_batch_no) + + return_entry = make_purchase_return(pr.name) + + return_entry.save() + return_entry.submit() + return_entry.reload() + + for row in return_entry.items: + if row.item_code == batch_item: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 300.00) + else: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 100.00) + + for row in return_entry.items: + if row.item_code == batch_item: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.rejected_serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 0) + else: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.rejected_serial_and_batch_bundle}, + pluck="incoming_rate", + ) + + for incoming_rate in bundle_data: + self.assertEqual(incoming_rate, 0) + + def test_purchase_return_partial_debit_note(self): + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work In Progress - TCP1", + ) + + return_pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work In Progress - TCP1", + is_return=1, + return_against=pr.name, + qty=-2, + do_not_submit=1, + ) + return_pr.items[0].purchase_receipt_item = pr.items[0].name + return_pr.submit() + + # because new_doc isn't considering is_return portion of status_updater + returned = frappe.get_doc("Purchase Receipt", return_pr.name) + returned.update_prevdoc_status() + pr.load_from_db() + + # Check if Original PR updated + self.assertEqual(pr.items[0].returned_qty, 2) + self.assertEqual(pr.per_returned, 40) + + # Create first partial debit_note + pi_1 = make_purchase_invoice(return_pr.name) + pi_1.items[0].qty = -1 + pi_1.submit() + + # Check if the first partial debit billing percentage got updated + return_pr.reload() + self.assertEqual(return_pr.per_billed, 50) + self.assertEqual(return_pr.status, "Partly Billed") + + # Create second partial debit_note to complete the debit note + pi_2 = make_purchase_invoice(return_pr.name) + pi_2.items[0].qty = -1 + pi_2.submit() + + # Check if the second partial debit note billing percentage got updated + return_pr.reload() + self.assertEqual(return_pr.per_billed, 100) + self.assertEqual(return_pr.status, "Completed") + + def test_do_not_allow_to_inward_same_serial_no_multiple_times(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + frappe.db.set_single_value("Stock Settings", "allow_existing_serial_no", 0) + + item_code = make_item( + "Test Do Not Allow INWD Item 123", {"has_serial_no": 1, "serial_no_series": "SN-TDAISN-.#####"} + ).name + + pr = make_purchase_receipt(item_code=item_code, qty=1, rate=100, use_serial_batch_fields=1) + serial_no = get_serial_nos_from_bundle(pr.items[0].serial_and_batch_bundle)[0] + + status = frappe.db.get_value("Serial No", serial_no, "status") + self.assertTrue(status == "Active") + + make_stock_entry( + item_code=item_code, + source=pr.items[0].warehouse, + qty=1, + serial_no=serial_no, + use_serial_batch_fields=1, + ) + + status = frappe.db.get_value("Serial No", serial_no, "status") + self.assertFalse(status == "Active") + + pr = make_purchase_receipt( + item_code=item_code, qty=1, rate=100, use_serial_batch_fields=1, do_not_submit=1 + ) + pr.items[0].serial_no = serial_no + pr.save() + + self.assertRaises(frappe.exceptions.ValidationError, pr.submit) + + frappe.db.set_single_value("Stock Settings", "allow_existing_serial_no", 1) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.js b/erpnext/stock/doctype/quality_inspection/quality_inspection.js index fc487514a2c7..99e25bf5eb6f 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.js +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.js @@ -60,7 +60,7 @@ frappe.ui.form.on("Quality Inspection", { refresh: function (frm) { // Ignore cancellation of reference doctype on cancel all. - frm.ignore_doctypes_on_cancel_all = [frm.doc.reference_type]; + frm.ignore_doctypes_on_cancel_all = [frm.doc.reference_type, "Serial and Batch Bundle"]; }, item_code: function (frm) { diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 60f048673fb8..6890256dc04e 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -6,7 +6,7 @@ from frappe import _ from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc -from frappe.utils import cint, cstr, flt, get_number_format_info +from frappe.utils import cint, cstr, flt, get_link_to_form, get_number_format_info from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template import ( get_template_details, @@ -73,6 +73,27 @@ def validate(self): if self.readings: self.inspect_and_set_status() + self.validate_inspection_required() + + def validate_inspection_required(self): + if self.reference_type in ["Purchase Receipt", "Purchase Invoice"] and not frappe.get_cached_value( + "Item", self.item_code, "inspection_required_before_purchase" + ): + frappe.throw( + _( + "'Inspection Required before Purchase' has disabled for the item {0}, no need to create the QI" + ).format(get_link_to_form("Item", self.item_code)) + ) + + if self.reference_type in ["Delivery Note", "Sales Invoice"] and not frappe.get_cached_value( + "Item", self.item_code, "inspection_required_before_delivery" + ): + frappe.throw( + _( + "'Inspection Required before Delivery' has disabled for the item {0}, no need to create the QI" + ).format(get_link_to_form("Item", self.item_code)) + ) + def before_submit(self): self.validate_readings_status_mandatory() @@ -109,6 +130,8 @@ def on_submit(self): self.update_qc_reference() def on_cancel(self): + self.ignore_linked_doctypes = "Serial and Batch Bundle" + self.update_qc_reference() def on_trash(self): @@ -243,6 +266,7 @@ def get_formula_evaluation_data(self, reading): for i in range(1, 11): field = "reading_" + str(i) if reading.get(field) is None: + data[field] = 0.0 continue data[field] = parse_float(reading.get(field)) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index c20eadeb78d9..0c81a296d5ee 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -125,7 +125,7 @@ def get_max_period_closing_date(company): query = ( frappe.qb.from_(table) - .select(Max(table.posting_date)) + .select(Max(table.period_end_date)) .where((table.company == company) & (table.docstatus == 1)) ).run() diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index ef4d475dca4d..a1f3135b70b8 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -89,6 +89,10 @@ def validate(self): self.validate_serial_and_batch_no() self.validate_duplicate_serial_and_batch_no() self.validate_voucher_no() + + if self.docstatus == 0: + self.allow_existing_serial_nos() + if self.type_of_transaction == "Maintenance": return @@ -102,6 +106,42 @@ def validate(self): self.set_incoming_rate() self.calculate_qty_and_amount() + def allow_existing_serial_nos(self): + if self.type_of_transaction == "Outward" or not self.has_serial_no: + return + + if frappe.db.get_single_value("Stock Settings", "allow_existing_serial_no"): + return + + if self.voucher_type not in ["Purchase Receipt", "Purchase Invoice", "Stock Entry"]: + return + + if self.voucher_type == "Stock Entry" and frappe.get_cached_value( + "Stock Entry", self.voucher_no, "purpose" + ) in ["Material Transfer", "Send to Subcontractor", "Material Transfer for Manufacture"]: + return + + serial_nos = [d.serial_no for d in self.entries if d.serial_no] + + data = frappe.get_all( + "Serial and Batch Entry", + filters={"serial_no": ("in", serial_nos), "docstatus": 1, "qty": ("<", 0)}, + fields=["serial_no", "parent"], + ) + + note = "

Note:
" + for row in data: + frappe.throw( + _( + "You can't process the serial number {0} as it has already been used in the SABB {1}. {2} if you want to inward same serial number multiple times then enabled 'Allow existing Serial No to be Manufactured/Received again' in the {3}" + ).format( + row.serial_no, + get_link_to_form("Serial and Batch Bundle", row.parent), + note, + get_link_to_form("Stock Settings", "Stock Settings"), + ) + ) + def reset_serial_batch_bundle(self): if self.is_new() and self.amended_from: for field in ["is_cancelled", "is_rejected"]: @@ -136,7 +176,12 @@ def validate_serial_nos_inventory(self): return serial_nos = [d.serial_no for d in self.entries if d.serial_no] - kwargs = {"item_code": self.item_code, "warehouse": self.warehouse} + kwargs = { + "item_code": self.item_code, + "warehouse": self.warehouse, + "check_serial_nos": True, + "serial_nos": serial_nos, + } if self.voucher_type == "POS Invoice": kwargs["ignore_voucher_nos"] = [self.voucher_no] @@ -177,6 +222,7 @@ def validate_serial_nos_duplicate(self): "posting_date": self.posting_date, "posting_time": self.posting_time, "serial_nos": serial_nos, + "check_serial_nos": True, } ) @@ -197,7 +243,7 @@ def validate_serial_nos_duplicate(self): def throw_error_message(self, message, exception=frappe.ValidationError): frappe.throw(_(message), exception, title=_("Error")) - def set_incoming_rate(self, row=None, save=False, allow_negative_stock=False): + def set_incoming_rate(self, parent=None, row=None, save=False, allow_negative_stock=False): if self.type_of_transaction not in ["Inward", "Outward"] or self.voucher_type in [ "Installation Note", "Job Card", @@ -206,13 +252,70 @@ def set_incoming_rate(self, row=None, save=False, allow_negative_stock=False): ]: return - if self.type_of_transaction == "Outward": + if return_aginst := self.get_return_aginst(parent=parent): + self.set_valuation_rate_for_return_entry(return_aginst, save) + elif self.type_of_transaction == "Outward": self.set_incoming_rate_for_outward_transaction( row, save, allow_negative_stock=allow_negative_stock ) else: self.set_incoming_rate_for_inward_transaction(row, save) + def set_valuation_rate_for_return_entry(self, return_aginst, save=False): + if valuation_details := self.get_valuation_rate_for_return_entry(return_aginst): + for row in self.entries: + if row.serial_no: + valuation_rate = valuation_details["serial_nos"].get(row.serial_no) + else: + valuation_rate = valuation_details["batches"].get(row.batch_no) + + row.incoming_rate = flt(valuation_rate) + row.stock_value_difference = flt(row.qty) * flt(row.incoming_rate) + + if save: + row.db_set( + { + "incoming_rate": row.incoming_rate, + "stock_value_difference": row.stock_value_difference, + } + ) + + def get_valuation_rate_for_return_entry(self, return_aginst): + valuation_details = frappe._dict( + { + "serial_nos": defaultdict(float), + "batches": defaultdict(float), + } + ) + + bundle_data = frappe.get_all( + "Serial and Batch Bundle", + fields=[ + "`tabSerial and Batch Entry`.`serial_no`", + "`tabSerial and Batch Entry`.`batch_no`", + "`tabSerial and Batch Entry`.`incoming_rate`", + ], + filters=[ + ["Serial and Batch Bundle", "voucher_no", "=", return_aginst], + ["Serial and Batch Entry", "docstatus", "=", 1], + ["Serial and Batch Bundle", "is_cancelled", "=", 0], + ["Serial and Batch Bundle", "item_code", "=", self.item_code], + ["Serial and Batch Bundle", "warehouse", "=", self.warehouse], + ], + order_by="`tabSerial and Batch Bundle`.`creation`, `tabSerial and Batch Entry`.`idx`", + ) + + if not bundle_data: + return {} + + for row in bundle_data: + if row.serial_no: + valuation_details["serial_nos"][row.serial_no] = row.incoming_rate + else: + valuation_details["batches"][row.batch_no] = row.incoming_rate + + return valuation_details + def calculate_total_qty(self, save=True): self.total_qty = 0.0 for d in self.entries: @@ -280,7 +383,7 @@ def set_incoming_rate_for_outward_transaction(self, row=None, save=False, allow_ ) def validate_negative_batch(self, batch_no, available_qty): - if available_qty < 0: + if available_qty < 0 and not self.is_stock_reco_for_valuation_adjustment(available_qty): msg = f"""Batch No {bold(batch_no)} of an Item {bold(self.item_code)} has negative stock of quantity {bold(available_qty)} in the @@ -288,6 +391,18 @@ def validate_negative_batch(self, batch_no, available_qty): frappe.throw(_(msg), BatchNegativeStockError) + def is_stock_reco_for_valuation_adjustment(self, available_qty): + if ( + self.voucher_type == "Stock Reconciliation" + and self.type_of_transaction == "Outward" + and self.voucher_detail_no + and abs(frappe.db.get_value("Stock Reconciliation Item", self.voucher_detail_no, "qty")) + == abs(available_qty) + ): + return True + + return False + def get_sle_for_outward_transaction(self): sle = frappe._dict( { @@ -315,6 +430,33 @@ def get_sle_for_outward_transaction(self): return sle + def get_return_aginst(self, parent=None): + return_aginst = None + + if parent and parent.get("is_return") and parent.get("return_against"): + return parent.get("return_against") + + if ( + self.voucher_type + in [ + "Delivery Note", + "Sales Invoice", + "Purchase Invoice", + "Purchase Receipt", + "POS Invoice", + "Subcontracting Receipt", + ] + and self.voucher_type + and self.voucher_no + ): + voucher_details = frappe.db.get_value( + self.voucher_type, self.voucher_no, ["is_return", "return_against"], as_dict=True + ) + if voucher_details and voucher_details.get("is_return") and voucher_details.get("return_against"): + return voucher_details.get("return_against") + + return return_aginst + def set_incoming_rate_for_inward_transaction(self, row=None, save=False): valuation_field = "valuation_rate" if self.voucher_type in ["Sales Invoice", "Delivery Note", "Quotation"]: @@ -336,18 +478,18 @@ def set_incoming_rate_for_inward_transaction(self, row=None, save=False): valuation_field = "rate" child_table = "Subcontracting Receipt Item" - precision = frappe.get_precision(child_table, valuation_field) or 2 - if not rate and self.voucher_detail_no and self.voucher_no: rate = frappe.db.get_value(child_table, self.voucher_detail_no, valuation_field) for d in self.entries: - if (d.incoming_rate == rate) and d.qty and d.stock_value_difference: + if self.is_rejected: + rate = 0.0 + elif (d.incoming_rate == rate) and d.qty and d.stock_value_difference: continue - d.incoming_rate = flt(rate, precision) + d.incoming_rate = rate if d.qty: - d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate) + d.stock_value_difference = d.qty * d.incoming_rate if save: d.db_set( @@ -391,7 +533,7 @@ def set_serial_and_batch_values(self, parent, row, qty_field=None): # If user has changed the rate in the child table if self.docstatus == 0: - self.set_incoming_rate(save=True, row=row) + self.set_incoming_rate(parent=parent, row=row, save=True) if self.docstatus == 0 and parent.get("is_return") and parent.is_new(): self.reset_qty(row, qty_field=qty_field) @@ -511,8 +653,10 @@ def validate_quantity(self, row, qty_field=None): precision = row.precision if abs(abs(flt(self.total_qty, precision)) - abs(flt(qty, precision))) > 0.01: + total_qty = frappe.format_value(abs(flt(self.total_qty)), "Float", row) + set_qty = frappe.format_value(abs(flt(row.get(qty_field))), "Float", row) self.throw_error_message( - f"Total quantity {abs(flt(self.total_qty))} in the Serial and Batch Bundle {bold(self.name)} does not match with the quantity {abs(flt(row.get(qty_field)))} for the Item {bold(self.item_code)} in the {self.voucher_type} # {self.voucher_no}" + f"Total quantity {total_qty} in the Serial and Batch Bundle {bold(self.name)} does not match with the quantity {set_qty} for the Item {bold(self.item_code)} in the {self.voucher_type} # {self.voucher_no}" ) def get_qty_field(self, row, qty_field=None) -> str: @@ -591,6 +735,9 @@ def validate_serial_and_batch_no(self): serial_batches = {} for row in self.entries: if not row.qty and row.batch_no and not row.serial_no: + if self.voucher_type == "Stock Reconciliation" and self.type_of_transaction == "Inward": + continue + frappe.throw( _("At row {0}: Qty is mandatory for the batch {1}").format( bold(row.idx), bold(row.batch_no) @@ -648,10 +795,6 @@ def validate_serial_batch_no(self, serial_batches): ) def validate_incorrect_serial_nos(self, serial_nos): - if self.voucher_type == "Stock Entry" and self.voucher_no: - if frappe.get_cached_value("Stock Entry", self.voucher_no, "purpose") == "Repack": - return - incorrect_serial_nos = frappe.get_all( "Serial No", filters={"name": ("in", serial_nos), "item_code": ("!=", self.item_code)}, @@ -875,6 +1018,9 @@ def validate_batch_inventory(self): ): return + if self.voucher_type in ["Sales Invoice", "Delivery Note"] and self.type_of_transaction == "Inward": + return + if not self.has_batch_no: return @@ -1296,8 +1442,12 @@ def add_serial_batch_ledgers(entries, child_row, doc, warehouse, do_not_save=Fal if parent_doc and isinstance(parent_doc, str): parent_doc = parse_json(parent_doc) - if frappe.db.exists("Serial and Batch Bundle", child_row.serial_and_batch_bundle): - sb_doc = update_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse) + bundle = child_row.serial_and_batch_bundle + if child_row.get("is_rejected"): + bundle = child_row.rejected_serial_and_batch_bundle + + if frappe.db.exists("Serial and Batch Bundle", bundle): + sb_doc = update_serial_batch_no_ledgers(bundle, entries, child_row, parent_doc, warehouse) else: sb_doc = create_serial_batch_no_ledgers( entries, child_row, parent_doc, warehouse, do_not_save=do_not_save @@ -1329,6 +1479,15 @@ def create_serial_batch_no_ledgers( } ) + batch_no = None + + if ( + not entries[0].get("batch_no") + and entries[0].get("serial_no") + and frappe.get_cached_value("Item", child_row.item_code, "has_batch_no") + ): + batch_no = get_batch(child_row.item_code) + for row in entries: row = frappe._dict(row) doc.append( @@ -1336,7 +1495,7 @@ def create_serial_batch_no_ledgers( { "qty": (flt(row.qty) or 1.0) * (1 if type_of_transaction == "Inward" else -1), "warehouse": warehouse, - "batch_no": row.batch_no, + "batch_no": row.batch_no or batch_no, "serial_no": row.serial_no, }, ) @@ -1351,6 +1510,18 @@ def create_serial_batch_no_ledgers( return doc +def get_batch(item_code): + from erpnext.stock.doctype.batch.batch import make_batch + + return make_batch( + frappe._dict( + { + "item": item_code, + } + ) + ) + + def get_type_of_transaction(parent_doc, child_row): type_of_transaction = child_row.get("type_of_transaction") if parent_doc.get("doctype") == "Stock Entry": @@ -1379,8 +1550,8 @@ def get_type_of_transaction(parent_doc, child_row): return type_of_transaction -def update_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse=None) -> object: - doc = frappe.get_doc("Serial and Batch Bundle", child_row.serial_and_batch_bundle) +def update_serial_batch_no_ledgers(bundle, entries, child_row, parent_doc, warehouse=None) -> object: + doc = frappe.get_doc("Serial and Batch Bundle", bundle) doc.voucher_detail_no = child_row.name doc.posting_date = parent_doc.posting_date doc.posting_time = parent_doc.posting_time @@ -1558,13 +1729,14 @@ def get_serial_nos_based_on_posting_date(kwargs, ignore_serial_nos): serial_nos = set() data = get_stock_ledgers_for_serial_nos(kwargs) + bundle_wise_serial_nos = get_bundle_wise_serial_nos(data, kwargs) for d in data: if d.serial_and_batch_bundle: - sns = get_serial_nos_from_bundle(d.serial_and_batch_bundle, kwargs.get("serial_nos", [])) - if d.actual_qty > 0: - serial_nos.update(sns) - else: - serial_nos.difference_update(sns) + if sns := bundle_wise_serial_nos.get(d.serial_and_batch_bundle): + if d.actual_qty > 0: + serial_nos.update(sns) + else: + serial_nos.difference_update(sns) elif d.serial_no: sns = get_serial_nos(d.serial_no) @@ -1581,6 +1753,30 @@ def get_serial_nos_based_on_posting_date(kwargs, ignore_serial_nos): return serial_nos +def get_bundle_wise_serial_nos(data, kwargs): + bundle_wise_serial_nos = defaultdict(list) + bundles = [d.serial_and_batch_bundle for d in data if d.serial_and_batch_bundle] + if not bundles: + return bundle_wise_serial_nos + + filters = {"parent": ("in", bundles), "docstatus": 1, "serial_no": ("is", "set")} + + if kwargs.get("check_serial_nos") and kwargs.get("serial_nos"): + filters["serial_no"] = ("in", kwargs.get("serial_nos")) + + bundle_data = frappe.get_all( + "Serial and Batch Entry", + fields=["serial_no", "parent"], + filters=filters, + ) + + for d in bundle_data: + if d.parent: + bundle_wise_serial_nos[d.parent].append(d.serial_no) + + return bundle_wise_serial_nos + + def get_reserved_serial_nos(kwargs) -> list: """Returns a list of `Serial No` reserved in POS Invoice and Stock Reservation Entry.""" @@ -2132,6 +2328,8 @@ def get_ledgers_from_serial_batch_bundle(**kwargs) -> list[frappe._dict]: def get_stock_ledgers_for_serial_nos(kwargs): + from erpnext.stock.utils import get_combine_datetime + stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry") query = ( @@ -2142,15 +2340,16 @@ def get_stock_ledgers_for_serial_nos(kwargs): stock_ledger_entry.serial_and_batch_bundle, ) .where(stock_ledger_entry.is_cancelled == 0) + .orderby(stock_ledger_entry.posting_datetime) ) if kwargs.get("posting_date"): if kwargs.get("posting_time") is None: kwargs.posting_time = nowtime() - timestamp_condition = CombineDatetime( - stock_ledger_entry.posting_date, stock_ledger_entry.posting_time - ) <= CombineDatetime(kwargs.posting_date, kwargs.posting_time) + timestamp_condition = stock_ledger_entry.posting_datetime <= get_combine_datetime( + kwargs.posting_date, kwargs.posting_time + ) query = query.where(timestamp_condition) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index c54876713c36..0e39c2a97565 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -117,6 +117,10 @@ frappe.ui.form.on("Stock Entry", { filters["is_inward"] = 1; } + if (["Material Receipt", "Material Transfer", "Material Issue"].includes(doc.purpose)) { + filters["include_expired_batches"] = 1; + } + return { query: "erpnext.controllers.queries.get_batch_no", filters: filters, @@ -447,9 +451,11 @@ frappe.ui.form.on("Stock Entry", { source_doctype: "Stock Entry", target: frm, date_field: "posting_date", + read_only_setters: ["stock_entry_type", "purpose", "add_to_transit"], setters: { stock_entry_type: "Material Transfer", purpose: "Material Transfer", + add_to_transit: 1, }, get_query_filters: { docstatus: 1, @@ -828,6 +834,15 @@ frappe.ui.form.on("Stock Entry", { }); frappe.ui.form.on("Stock Entry Detail", { + set_basic_rate_manually(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + frm.fields_dict.items.grid.update_docfield_property( + "basic_rate", + "read_only", + row?.set_basic_rate_manually ? 0 : 1 + ); + }, + qty(frm, cdt, cdn) { frm.events.set_basic_rate(frm, cdt, cdn); }, @@ -1296,7 +1311,7 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle this.frm.cscript.toggle_enable_bom(); - if (doc.purpose == "Send to Subcontractor") { + if (erpnext.stock.is_subcontracting_or_return_transfer(doc)) { doc.customer = doc.customer_name = doc.customer_address = @@ -1362,6 +1377,10 @@ erpnext.stock.select_batch_and_serial_no = (frm, item) => { }); }; +erpnext.stock.is_subcontracting_or_return_transfer = (doc) => { + return doc.purpose == "Send to Subcontractor" || (doc.purpose == "Material Transfer" && doc.is_return); +}; + function attach_bom_items(bom_no) { if (!bom_no) { return; diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 3f467d3627ac..f5fdfafe778c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -127,7 +127,7 @@ "label": "Purpose", "oldfieldname": "purpose", "oldfieldtype": "Select", - "options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor", + "options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor\nDisassemble", "read_only": 1, "search_index": 1 }, @@ -143,7 +143,7 @@ "reqd": 1 }, { - "depends_on": "eval:in_list([\"Material Transfer for Manufacture\", \"Manufacture\", \"Material Consumption for Manufacture\"], doc.purpose)", + "depends_on": "eval:in_list([\"Material Transfer for Manufacture\", \"Manufacture\", \"Material Consumption for Manufacture\", \"Disassemble\"], doc.purpose)", "fieldname": "work_order", "fieldtype": "Link", "label": "Work Order", @@ -154,14 +154,14 @@ "search_index": 1 }, { - "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"", + "depends_on": "eval: erpnext.stock.is_subcontracting_or_return_transfer(doc)", "fieldname": "purchase_order", "fieldtype": "Link", "label": "Purchase Order", "options": "Purchase Order" }, { - "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"", + "depends_on": "eval: erpnext.stock.is_subcontracting_or_return_transfer(doc)", "fieldname": "subcontracting_order", "fieldtype": "Link", "label": "Subcontracting Order", @@ -242,7 +242,7 @@ }, { "default": "0", - "depends_on": "eval:in_list([\"Material Issue\", \"Material Transfer\", \"Manufacture\", \"Repack\", \"Send to Subcontractor\", \"Material Transfer for Manufacture\", \"Material Consumption for Manufacture\"], doc.purpose)", + "depends_on": "eval:in_list([\"Material Issue\", \"Material Transfer\", \"Manufacture\", \"Repack\", \"Send to Subcontractor\", \"Material Transfer for Manufacture\", \"Material Consumption for Manufacture\", \"Disassemble\"], doc.purpose)", "fieldname": "from_bom", "fieldtype": "Check", "label": "From BOM", @@ -427,13 +427,13 @@ }, { "collapsible": 1, - "depends_on": "eval:doc.purpose === \"Send to Subcontractor\"", + "depends_on": "eval: erpnext.stock.is_subcontracting_or_return_transfer(doc)", "fieldname": "contact_section", "fieldtype": "Section Break", "label": "Supplier Details" }, { - "depends_on": "eval:doc.purpose === \"Send to Subcontractor\"", + "depends_on": "eval: erpnext.stock.is_subcontracting_or_return_transfer(doc)", "fieldname": "supplier", "fieldtype": "Link", "label": "Supplier", @@ -445,7 +445,7 @@ }, { "bold": 1, - "depends_on": "eval:doc.purpose === \"Send to Subcontractor\"", + "depends_on": "eval: erpnext.stock.is_subcontracting_or_return_transfer(doc)", "fieldname": "supplier_name", "fieldtype": "Data", "label": "Supplier Name", @@ -455,7 +455,7 @@ "read_only": 1 }, { - "depends_on": "eval:doc.purpose === \"Send to Subcontractor\"", + "depends_on": "eval: erpnext.stock.is_subcontracting_or_return_transfer(doc)", "fieldname": "supplier_address", "fieldtype": "Link", "label": "Supplier Address", @@ -697,7 +697,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-08-13 19:02:42.386955", + "modified": "2024-08-13 19:05:42.386955", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 14e0fdf6388a..f362f9d3da9f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -132,6 +132,7 @@ class StockEntry(StockController): "Manufacture", "Repack", "Send to Subcontractor", + "Disassemble", ] remarks: DF.Text | None sales_invoice_no: DF.Link | None @@ -231,6 +232,7 @@ def validate(self): self.validate_serialized_batch() self.calculate_rate_and_amount() self.validate_putaway_capacity() + self.validate_component_quantities() if not self.get("purpose") == "Manufacture": # ignore scrap item wh difference and empty source/target wh @@ -337,6 +339,7 @@ def validate_purpose(self): "Repack", "Send to Subcontractor", "Material Consumption for Manufacture", + "Disassemble", ] if self.purpose not in valid_purposes: @@ -616,6 +619,7 @@ def validate_work_order(self): "Manufacture", "Material Transfer for Manufacture", "Material Consumption for Manufacture", + "Disassemble", ): # check if work order is entered @@ -744,6 +748,34 @@ def set_actual_qty(self): title=_("Insufficient Stock"), ) + def validate_component_quantities(self): + if self.purpose not in ["Manufacture", "Material Transfer for Manufacture"]: + return + + if not frappe.db.get_single_value("Manufacturing Settings", "validate_components_quantities_per_bom"): + return + + if not self.fg_completed_qty: + return + + raw_materials = self.get_bom_raw_materials(self.fg_completed_qty) + + precision = frappe.get_precision("Stock Entry Detail", "qty") + for row in self.items: + if not row.s_warehouse: + continue + + if details := raw_materials.get(row.item_code): + if flt(details.get("qty"), precision) != flt(row.qty, precision): + frappe.throw( + _("For the item {0}, the quantity should be {1} according to the BOM {2}.").format( + frappe.bold(row.item_code), + flt(details.get("qty"), precision), + get_link_to_form("BOM", self.bom_no), + ), + title=_("Incorrect Component Quantity"), + ) + @frappe.whitelist() def get_stock_and_rate(self): """ @@ -763,9 +795,6 @@ def calculate_rate_and_amount(self, reset_outgoing_rate=True, raise_error_if_no_ self.set_total_incoming_outgoing_value() self.set_total_amount() - if not reset_outgoing_rate: - self.set_serial_and_batch_bundle() - def set_basic_rate(self, reset_outgoing_rate=True, raise_error_if_no_rate=True): """ Set rate for outgoing, scrapped and finished items @@ -898,8 +927,8 @@ def get_basic_rate_for_manufactured_item(self, finished_item_qty, outgoing_items ).format( item.idx, frappe.bold(label), - frappe.bold("Manufacture"), - frappe.bold("Material Consumption for Manufacture"), + frappe.bold(_("Manufacture")), + frappe.bold(_("Material Consumption for Manufacture")), ) ) @@ -909,7 +938,7 @@ def get_basic_rate_for_manufactured_item(self, finished_item_qty, outgoing_items ): frappe.throw( _("Only one {0} entry can be created against the Work Order {1}").format( - frappe.bold("Manufacture"), frappe.bold(self.work_order) + frappe.bold(_("Manufacture")), frappe.bold(self.work_order) ) ) @@ -983,7 +1012,7 @@ def set_total_amount(self): def set_stock_entry_type(self): if self.purpose: self.stock_entry_type = frappe.get_cached_value( - "Stock Entry Type", {"purpose": self.purpose}, "name" + "Stock Entry Type", {"purpose": self.purpose, "is_standard": 1}, "name" ) def set_purpose_for_stock_entry(self): @@ -1277,10 +1306,10 @@ def validate_finished_goods(self): 3. Check FG Item and Qty against WO if present (mfg) """ production_item, wo_qty, finished_items = None, 0, [] - - wo_details = frappe.db.get_value("Work Order", self.work_order, ["production_item", "qty"]) - if wo_details: - production_item, wo_qty = wo_details + if self.work_order: + wo_details = frappe.db.get_value("Work Order", self.work_order, ["production_item", "qty"]) + if wo_details: + production_item, wo_qty = wo_details for d in self.get("items"): if d.is_finished_item: @@ -1562,10 +1591,6 @@ def _validate_work_order(pro_doc): if pro_doc.status == "Stopped": msg = f"Transaction not allowed against stopped Work Order {self.work_order}" - if self.is_return and pro_doc.status not in ["Completed", "Closed"]: - title = _("Stock Return") - msg = f"Work Order {self.work_order} must be completed or closed" - if msg: frappe.throw(_(msg), title=title) @@ -1703,11 +1728,63 @@ def set_items_for_stock_in(self): }, ) + def get_items_for_disassembly(self): + """Get items for Disassembly Order""" + + if not self.work_order: + frappe.throw(_("The Work Order is mandatory for Disassembly Order")) + + items = self.get_items_from_manufacture_entry() + + s_warehouse = "" + if self.work_order: + s_warehouse = frappe.db.get_value("Work Order", self.work_order, "fg_warehouse") + + for row in items: + child_row = self.append("items", {}) + for field, value in row.items(): + if value is not None: + child_row.set(field, value) + + child_row.s_warehouse = (self.from_warehouse or s_warehouse) if row.is_finished_item else "" + child_row.t_warehouse = self.to_warehouse if not row.is_finished_item else "" + child_row.is_finished_item = 0 if row.is_finished_item else 1 + + def get_items_from_manufacture_entry(self): + return frappe.get_all( + "Stock Entry", + fields=[ + "`tabStock Entry Detail`.`item_code`", + "`tabStock Entry Detail`.`item_name`", + "`tabStock Entry Detail`.`description`", + "`tabStock Entry Detail`.`qty`", + "`tabStock Entry Detail`.`transfer_qty`", + "`tabStock Entry Detail`.`stock_uom`", + "`tabStock Entry Detail`.`uom`", + "`tabStock Entry Detail`.`basic_rate`", + "`tabStock Entry Detail`.`conversion_factor`", + "`tabStock Entry Detail`.`is_finished_item`", + "`tabStock Entry Detail`.`batch_no`", + "`tabStock Entry Detail`.`serial_no`", + "`tabStock Entry Detail`.`use_serial_batch_fields`", + ], + filters=[ + ["Stock Entry", "purpose", "=", "Manufacture"], + ["Stock Entry", "work_order", "=", self.work_order], + ["Stock Entry", "docstatus", "=", 1], + ["Stock Entry Detail", "docstatus", "=", 1], + ], + order_by="`tabStock Entry Detail`.`idx` desc, `tabStock Entry Detail`.`is_finished_item` desc", + ) + @frappe.whitelist() def get_items(self): self.set("items", []) self.validate_work_order() + if self.purpose == "Disassemble": + return self.get_items_for_disassembly() + if not self.posting_date or not self.posting_time: frappe.throw(_("Posting date and posting time is mandatory")) @@ -3064,11 +3141,13 @@ def get_available_materials(work_order) -> dict: if row.serial_no: for serial_no in get_serial_nos(row.serial_no): - item_data.serial_nos.remove(serial_no) + if serial_no in item_data.serial_nos: + item_data.serial_nos.remove(serial_no) elif row.serial_nos: for serial_no in get_serial_nos(row.serial_nos): - item_data.serial_nos.remove(serial_no) + if serial_no in item_data.serial_nos: + item_data.serial_nos.remove(serial_no) return available_materials @@ -3186,6 +3265,9 @@ def create_serial_and_batch_bundle(parent_doc, row, child, type_of_transaction=N for batch_no, qty in row.batches_to_be_consume.items(): doc.append("entries", {"batch_no": batch_no, "warehouse": row.warehouse, "qty": qty * -1}) + if not doc.entries: + return None + return doc.insert(ignore_permissions=True).name diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index a680b7733d3c..a26940462bf1 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -4,7 +4,7 @@ from frappe.permissions import add_user_permission, remove_user_permission from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import add_days, flt, nowtime, today +from frappe.utils import add_days, cstr, flt, get_time, getdate, nowtime, today from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.stock.doctype.item.test_item import ( @@ -970,61 +970,6 @@ def test_nagative_stock_for_batch(self): self.assertRaises(frappe.ValidationError, ste.submit) - def test_same_serial_nos_in_repack_or_manufacture_entries(self): - s1 = make_serialized_item(target_warehouse="_Test Warehouse - _TC") - serial_nos = get_serial_nos_from_bundle(s1.get("items")[0].serial_and_batch_bundle) - - s2 = make_stock_entry( - item_code="_Test Serialized Item With Series", - source="_Test Warehouse - _TC", - qty=2, - basic_rate=100, - purpose="Repack", - serial_no=serial_nos, - do_not_save=True, - ) - - frappe.flags.use_serial_and_batch_fields = True - - cls_obj = SerialBatchCreation( - { - "type_of_transaction": "Inward", - "serial_and_batch_bundle": s2.items[0].serial_and_batch_bundle, - "item_code": "_Test Serialized Item", - "warehouse": "_Test Warehouse - _TC", - } - ) - - cls_obj.duplicate_package() - bundle_id = cls_obj.serial_and_batch_bundle - doc = frappe.get_doc("Serial and Batch Bundle", bundle_id) - doc.db_set( - { - "item_code": "_Test Serialized Item", - "warehouse": "_Test Warehouse - _TC", - } - ) - - doc.load_from_db() - - s2.append( - "items", - { - "item_code": "_Test Serialized Item", - "t_warehouse": "_Test Warehouse - _TC", - "qty": 2, - "basic_rate": 120, - "expense_account": "Stock Adjustment - _TC", - "conversion_factor": 1.0, - "cost_center": "_Test Cost Center - _TC", - "serial_and_batch_bundle": bundle_id, - }, - ) - - s2.submit() - s2.cancel() - frappe.flags.use_serial_and_batch_fields = False - def test_quality_check(self): item_code = "_Test Item For QC" if not frappe.db.exists("Item", item_code): @@ -1700,6 +1645,46 @@ def test_auto_reorder_level(self): mr.cancel() mr.delete() + def test_auto_reorder_level_with_lead_time_days(self): + from erpnext.stock.reorder_item import reorder_item + + item_doc = make_item( + "Test Auto Reorder Item - 002", + properties={"stock_uom": "Kg", "purchase_uom": "Nos", "is_stock_item": 1, "lead_time_days": 2}, + uoms=[{"uom": "Nos", "conversion_factor": 5}], + ) + + if not frappe.db.exists("Item Reorder", {"parent": item_doc.name}): + item_doc.append( + "reorder_levels", + { + "warehouse_reorder_level": 0, + "warehouse_reorder_qty": 10, + "warehouse": "_Test Warehouse - _TC", + "material_request_type": "Purchase", + }, + ) + + item_doc.save(ignore_permissions=True) + + frappe.db.set_single_value("Stock Settings", "auto_indent", 1) + + mr_list = reorder_item() + + frappe.db.set_single_value("Stock Settings", "auto_indent", 0) + mrs = frappe.get_all( + "Material Request Item", + fields=["schedule_date"], + filters={"item_code": item_doc.name, "uom": "Nos"}, + ) + + for mri in mrs: + self.assertEqual(getdate(mri.schedule_date), getdate(add_days(today(), 2))) + + for mr in mr_list: + mr.cancel() + mr.delete() + def test_use_serial_and_batch_fields(self): item = make_item( "Test Use Serial and Batch Item SN Item", @@ -1780,6 +1765,74 @@ def test_serial_batch_bundle_type_of_transaction(self): frappe.db.set_value("Serial and Batch Bundle", sbb, "type_of_transaction", "Inward") self.assertRaises(frappe.ValidationError, se.submit) + def test_stock_entry_for_same_posting_date_and_time(self): + warehouse = "_Test Warehouse - _TC" + item_code = "Test Stock Entry For Same Posting Datetime 1" + make_item(item_code, {"is_stock_item": 1}) + posting_date = nowdate() + posting_time = nowtime() + + for index in range(25): + se = make_stock_entry( + item_code=item_code, + qty=1, + to_warehouse=warehouse, + posting_date=posting_date, + posting_time=posting_time, + do_not_submit=True, + purpose="Material Receipt", + basic_rate=100, + ) + + se.append( + "items", + { + "item_code": item_code, + "item_name": se.items[0].item_name, + "description": se.items[0].description, + "t_warehouse": se.items[0].t_warehouse, + "basic_rate": 100, + "qty": 1, + "stock_qty": 1, + "conversion_factor": 1, + "expense_account": se.items[0].expense_account, + "cost_center": se.items[0].cost_center, + "uom": se.items[0].uom, + "stock_uom": se.items[0].stock_uom, + }, + ) + + se.remarks = f"The current number is {cstr(index)}" + + se.submit() + + sles = frappe.get_all( + "Stock Ledger Entry", + fields=[ + "posting_date", + "posting_time", + "actual_qty", + "qty_after_transaction", + "incoming_rate", + "stock_value_difference", + "stock_value", + ], + filters={"item_code": item_code, "warehouse": warehouse}, + order_by="creation", + ) + + self.assertEqual(len(sles), 50) + i = 0 + for sle in sles: + i += 1 + self.assertEqual(getdate(sle.posting_date), getdate(posting_date)) + self.assertEqual(get_time(sle.posting_time), get_time(posting_time)) + self.assertEqual(sle.actual_qty, 1) + self.assertEqual(sle.qty_after_transaction, i) + self.assertEqual(sle.incoming_rate, 100) + self.assertEqual(sle.stock_value_difference, 100) + self.assertEqual(sle.stock_value, 100 * i) + def make_serialized_item(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json index db8e1fef69ba..c522df599416 100644 --- a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json +++ b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json @@ -7,7 +7,8 @@ "engine": "InnoDB", "field_order": [ "purpose", - "add_to_transit" + "add_to_transit", + "is_standard" ], "fields": [ { @@ -16,7 +17,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Purpose", - "options": "\nMaterial Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor", + "options": "\nMaterial Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor\nDisassemble", "reqd": 1, "set_only_once": 1 }, @@ -26,13 +27,21 @@ "fieldname": "add_to_transit", "fieldtype": "Check", "label": "Add to Transit" + }, + { + "default": "0", + "fieldname": "is_standard", + "fieldtype": "Check", + "label": "Is Standard", + "read_only": 1 } ], "links": [], - "modified": "2024-07-08 08:41:19.385020", + "modified": "2024-08-24 16:00:22.696958", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Type", + "naming_rule": "Set by user", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py index 034223122f60..8d05a43408b9 100644 --- a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py +++ b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py @@ -2,7 +2,7 @@ # For license information, please see license.txt -# import frappe +import frappe from frappe.model.document import Document @@ -16,6 +16,7 @@ class StockEntryType(Document): from frappe.types import DF add_to_transit: DF.Check + is_standard: DF.Check purpose: DF.Literal[ "", "Material Issue", @@ -26,9 +27,25 @@ class StockEntryType(Document): "Manufacture", "Repack", "Send to Subcontractor", + "Disassemble", ] # end: auto-generated types def validate(self): + self.validate_standard_type() if self.add_to_transit and self.purpose != "Material Transfer": self.add_to_transit = 0 + + def validate_standard_type(self): + if self.is_standard and self.name not in [ + "Material Issue", + "Material Receipt", + "Material Transfer", + "Material Transfer for Manufacture", + "Material Consumption for Manufacture", + "Manufacture", + "Repack", + "Send to Subcontractor", + "Disassemble", + ]: + frappe.throw(f"Stock Entry Type {self.name} cannot be set as standard") diff --git a/erpnext/stock/doctype/stock_entry_type/test_stock_entry_type.py b/erpnext/stock/doctype/stock_entry_type/test_stock_entry_type.py index 83ebe7e651b5..61156545e340 100644 --- a/erpnext/stock/doctype/stock_entry_type/test_stock_entry_type.py +++ b/erpnext/stock/doctype/stock_entry_type/test_stock_entry_type.py @@ -3,6 +3,33 @@ import unittest +import frappe + class TestStockEntryType(unittest.TestCase): - pass + def test_stock_entry_type_non_standard(self): + stock_entry_type = "Test Manufacturing" + + doc = frappe.get_doc( + { + "doctype": "Stock Entry Type", + "__newname": stock_entry_type, + "purpose": "Manufacture", + "is_standard": 1, + } + ) + + self.assertRaises(frappe.ValidationError, doc.insert) + + def test_stock_entry_type_is_standard(self): + for stock_entry_type in [ + "Material Issue", + "Material Receipt", + "Material Transfer", + "Material Transfer for Manufacture", + "Material Consumption for Manufacture", + "Manufacture", + "Repack", + "Send to Subcontractor", + ]: + self.assertTrue(frappe.db.get_value("Stock Entry Type", stock_entry_type, "is_standard")) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json index 58b6e4a74b60..d9af64c3983a 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json @@ -352,7 +352,8 @@ { "fieldname": "posting_datetime", "fieldtype": "Datetime", - "label": "Posting Datetime" + "label": "Posting Datetime", + "search_index": 1 } ], "hide_toolbar": 1, @@ -361,7 +362,7 @@ "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2024-06-27 16:23:18.820049", + "modified": "2024-08-27 09:28:03.961443", "modified_by": "Administrator", "module": "Stock", "name": "Stock Ledger Entry", diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 319303dbbb0d..5aeabeeec56c 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -8,6 +8,7 @@ from frappe import _, bold from frappe.core.doctype.role.role import get_users from frappe.model.document import Document +from frappe.query_builder.functions import Sum from frappe.utils import add_days, cint, flt, formatdate, get_datetime, getdate from erpnext.accounts.utils import get_fiscal_year @@ -25,6 +26,10 @@ class BackDatedStockTransaction(frappe.ValidationError): pass +class InventoryDimensionNegativeStockError(frappe.ValidationError): + pass + + exclude_from_linked_with = True @@ -104,61 +109,56 @@ def set_posting_datetime(self, save=False): self.posting_datetime = get_combine_datetime(self.posting_date, self.posting_time) def validate_inventory_dimension_negative_stock(self): - if self.is_cancelled: + if self.is_cancelled or self.actual_qty >= 0: return - extra_cond = "" - kwargs = {} - dimensions = self._get_inventory_dimensions() if not dimensions: return + flt_precision = cint(frappe.db.get_default("float_precision")) or 2 for dimension, values in dimensions.items(): - kwargs[dimension] = values.get("value") - extra_cond += f" and {dimension} = %({dimension})s" - - kwargs.update( - { - "item_code": self.item_code, - "warehouse": self.warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "company": self.company, - "sle": self.name, - } - ) + dimension_value = values.get("value") + available_qty = self.get_available_qty_after_prev_transaction(dimension, dimension_value) + + diff = flt(available_qty + flt(self.actual_qty), flt_precision) # qty after current transaction + if diff < 0 and abs(diff) > 0.0001: + self.throw_validation_error(diff, dimension, dimension_value) + + def get_available_qty_after_prev_transaction(self, dimension, dimension_value): + sle = frappe.qb.DocType("Stock Ledger Entry") + available_qty = ( + frappe.qb.from_(sle) + .select(Sum(sle.actual_qty)) + .where( + (sle.item_code == self.item_code) + & (sle.warehouse == self.warehouse) + & (sle.posting_datetime < self.posting_datetime) + & (sle.company == self.company) + & (sle.is_cancelled == 0) + & (sle[dimension] == dimension_value) + ) + ).run() - sle = get_previous_sle(kwargs, extra_cond=extra_cond) - qty_after_transaction = 0.0 - flt_precision = cint(frappe.db.get_default("float_precision")) or 2 - if sle: - qty_after_transaction = sle.qty_after_transaction - - diff = qty_after_transaction + flt(self.actual_qty) - diff = flt(diff, flt_precision) - if diff < 0 and abs(diff) > 0.0001: - self.throw_validation_error(diff, dimensions) - - def throw_validation_error(self, diff, dimensions): - dimension_msg = _(", with the inventory {0}: {1}").format( - "dimensions" if len(dimensions) > 1 else "dimension", - ", ".join(f"{bold(d.doctype)} ({d.value})" for k, d in dimensions.items()), - ) + return available_qty[0][0] or 0 + def throw_validation_error(self, diff, dimension, dimension_value): msg = _( - "{0} units of {1} are required in {2}{3}, on {4} {5} for {6} to complete the transaction." + "{0} units of {1} are required in {2} with the inventory dimension: {3} ({4}) on {5} {6} for {7} to complete the transaction." ).format( abs(diff), frappe.get_desk_link("Item", self.item_code), frappe.get_desk_link("Warehouse", self.warehouse), - dimension_msg, + frappe.bold(dimension), + frappe.bold(dimension_value), self.posting_date, self.posting_time, frappe.get_desk_link(self.voucher_type, self.voucher_no), ) - frappe.throw(msg, title=_("Inventory Dimension Negative Stock")) + frappe.throw( + msg, title=_("Inventory Dimension Negative Stock"), exc=InventoryDimensionNegativeStockError + ) def _get_inventory_dimensions(self): inv_dimensions = get_inventory_dimensions() 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 069192fdb160..42e402e00059 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 @@ -1043,6 +1043,8 @@ def _day(days): self.assertEqual(50, _get_stock_credit(final_consumption)) def test_tie_breaking(self): + from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import repost_entries + frappe.flags.dont_execute_stock_reposts = True self.addCleanup(frappe.flags.pop, "dont_execute_stock_reposts") @@ -1085,6 +1087,7 @@ def ordered_qty_after_transaction(): self.assertEqual([10, 11], ordered_qty_after_transaction()) first.cancel() + repost_entries() self.assertEqual([1], ordered_qty_after_transaction()) backdated = make_stock_entry( @@ -1184,7 +1187,7 @@ def test_backdated_sle_with_same_timestamp(self): qty=5, posting_date="2021-01-01", rate=10, - posting_time="02:00:00.1234", + posting_time="02:00:00", ) time.sleep(3) @@ -1196,7 +1199,7 @@ def test_backdated_sle_with_same_timestamp(self): qty=100, rate=10, posting_date="2021-01-01", - posting_time="02:00:00", + posting_time="02:00:00.1234", ) sle = frappe.get_all( diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 93e3e69729b1..c13b3620517f 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -61,6 +61,7 @@ def __init__(self, *args, **kwargs): self.head_row = ["Item Code", "Warehouse", "Quantity", "Valuation Rate"] def validate(self): + self.validate_items_exist() if not self.expense_account: self.expense_account = frappe.get_cached_value( "Company", self.company, "stock_adjustment_account" @@ -162,6 +163,27 @@ def make_bundle_for_current_qty(self): def set_current_serial_and_batch_bundle(self, voucher_detail_no=None, save=False) -> None: """Set Serial and Batch Bundle for each item""" for item in self.items: + if not frappe.db.exists("Item", item.item_code): + frappe.throw(_("Item {0} does not exist").format(item.item_code)) + + item_details = frappe.get_cached_value( + "Item", item.item_code, ["has_serial_no", "has_batch_no"], as_dict=1 + ) + + if not (item_details.has_serial_no or item_details.has_batch_no): + continue + + if ( + not item.use_serial_batch_fields + and not item.reconcile_all_serial_batch + and not item.serial_and_batch_bundle + ): + frappe.throw( + _("Row # {0}: Please add Serial and Batch Bundle for Item {1}").format( + item.idx, frappe.bold(item.item_code) + ) + ) + if not item.reconcile_all_serial_batch and item.serial_and_batch_bundle: bundle = self.get_bundle_for_specific_serial_batch(item) item.current_serial_and_batch_bundle = bundle.name @@ -177,13 +199,6 @@ def set_current_serial_and_batch_bundle(self, voucher_detail_no=None, save=False if voucher_detail_no and voucher_detail_no != item.name: continue - item_details = frappe.get_cached_value( - "Item", item.item_code, ["has_serial_no", "has_batch_no"], as_dict=1 - ) - - if not (item_details.has_serial_no or item_details.has_batch_no): - continue - if not item.current_serial_and_batch_bundle: serial_and_batch_bundle = frappe.get_doc( { @@ -320,6 +335,7 @@ def get_bundle_for_specific_serial_batch(self, row) -> str: row.item_code, posting_date=self.posting_date, posting_time=self.posting_time, + for_stock_levels=True, ) total_current_qty += current_qty @@ -357,6 +373,9 @@ def has_change_in_serial_batch(self, row) -> bool: def set_new_serial_and_batch_bundle(self): for item in self.items: + if not item.item_code: + continue + if item.use_serial_batch_fields: continue @@ -392,6 +411,28 @@ def set_new_serial_and_batch_bundle(self): item.qty = bundle_doc.total_qty item.valuation_rate = bundle_doc.avg_rate + elif item.serial_and_batch_bundle and item.qty: + self.update_existing_serial_and_batch_bundle(item) + + def update_existing_serial_and_batch_bundle(self, item): + batch_details = frappe.get_all( + "Serial and Batch Entry", + fields=["batch_no", "qty", "name"], + filters={"parent": item.serial_and_batch_bundle, "batch_no": ("is", "set")}, + ) + + if batch_details and len(batch_details) == 1: + batch = batch_details[0] + if abs(batch.qty) == abs(item.qty): + return + + update_values = { + "qty": item.qty, + "stock_value_difference": flt(item.valuation_rate) * flt(item.qty), + } + + frappe.db.set_value("Serial and Batch Entry", batch.name, update_values) + def remove_items_with_no_change(self): """Remove items if qty or rate is not changed""" self.difference_amount = 0.0 @@ -684,7 +725,7 @@ def make_adjustment_entry(self, row, sl_entries): from erpnext.stock.stock_ledger import get_stock_value_difference difference_amount = get_stock_value_difference( - row.item_code, row.warehouse, self.posting_date, self.posting_time + row.item_code, row.warehouse, self.posting_date, self.posting_time, self.name ) if not difference_amount: @@ -1315,7 +1356,16 @@ def get_stock_balance_for( qty, rate = data if item_dict.get("has_batch_no"): - qty = get_batch_qty(batch_no, warehouse, posting_date=posting_date, posting_time=posting_time) or 0 + qty = ( + get_batch_qty( + batch_no, + warehouse, + posting_date=posting_date, + posting_time=posting_time, + for_stock_levels=True, + ) + or 0 + ) return { "qty": qty, diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 9bb6ba9ec90e..9754443d4ac9 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -1275,6 +1275,60 @@ def test_stock_reco_with_legacy_batch(self): qty = get_batch_qty(batch_id, warehouse, batch_item_code) self.assertEqual(qty, 110) + def test_skip_reposting_for_entries_after_stock_reco(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item_code = create_item("Test Item For Skip Reposting After Stock Reco", is_stock_item=1).name + + warehouse = "_Test Warehouse - _TC" + + make_stock_entry( + posting_date="2024-11-01", + posting_time="11:00", + item_code=item_code, + target=warehouse, + qty=10, + basic_rate=100, + ) + + create_stock_reconciliation( + posting_date="2024-11-02", + posting_time="11:00", + item_code=item_code, + warehouse=warehouse, + qty=20, + rate=100, + ) + + se = make_stock_entry( + posting_date="2024-11-03", + posting_time="11:00", + item_code=item_code, + source=warehouse, + qty=15, + ) + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": se.name, "is_cancelled": 0}, "stock_value_difference" + ) + + self.assertEqual(stock_value_difference, 1500.00 * -1) + + make_stock_entry( + posting_date="2024-10-29", + posting_time="11:00", + item_code=item_code, + target=warehouse, + qty=10, + basic_rate=100, + ) + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": se.name, "is_cancelled": 0}, "stock_value_difference" + ) + + self.assertEqual(stock_value_difference, 1500.00 * -1) + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py index 9808d2dfe72b..8316488e253c 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -255,7 +255,7 @@ def validate_reservation_based_on_serial_and_batch(self) -> None: if self.has_batch_no else _("Warehouse"), frappe.bold(self.warehouse), - frappe.bold("Stock Reservation Entry"), + frappe.bold(_("Stock Reservation Entry")), ) frappe.throw(msg) @@ -497,7 +497,8 @@ def validate_stock_reservation_settings(voucher: object) -> None: if not frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"): msg = _("Please enable {0} in the {1}.").format( - frappe.bold("Stock Reservation"), frappe.bold("Stock Settings") + frappe.bold(_("Stock Reservation")), + frappe.bold(_("Stock Settings")), ) frappe.throw(msg) diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index 069e7da41cb3..e542a1582e38 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -49,12 +49,13 @@ "do_not_use_batchwise_valuation", "auto_create_serial_and_batch_bundle_for_outward", "pick_serial_and_batch_based_on", + "naming_series_prefix", "column_break_mhzc", "disable_serial_no_and_batch_selector", "use_naming_series", - "naming_series_prefix", "use_serial_batch_fields", "do_not_update_serial_batch_on_creation_of_auto_bundle", + "allow_existing_serial_no", "stock_planning_tab", "auto_material_request", "auto_indent", @@ -460,6 +461,12 @@ "fieldname": "over_picking_allowance", "fieldtype": "Percent", "label": "Over Picking Allowance" + }, + { + "default": "1", + "fieldname": "allow_existing_serial_no", + "fieldtype": "Check", + "label": "Allow existing Serial No to be Manufactured/Received again" } ], "icon": "icon-cog", @@ -467,7 +474,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-07-29 14:55:19.093508", + "modified": "2024-12-09 17:52:36.030456", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index c029b7bd1fbf..b7a317cd66a3 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -25,6 +25,7 @@ class StockSettings(Document): action_if_quality_inspection_is_not_submitted: DF.Literal["Stop", "Warn"] action_if_quality_inspection_is_rejected: DF.Literal["Stop", "Warn"] + allow_existing_serial_no: DF.Check allow_from_dn: DF.Check allow_from_pr: DF.Check allow_internal_transfer_at_arms_length_price: DF.Check @@ -175,7 +176,7 @@ def validate_stock_reservation(self): if self.allow_negative_stock and self.enable_stock_reservation: frappe.throw( _("As {0} is enabled, you can not enable {1}.").format( - frappe.bold("Stock Reservation"), frappe.bold("Allow Negative Stock") + frappe.bold(_("Stock Reservation")), frappe.bold(_("Allow Negative Stock")) ) ) @@ -187,7 +188,7 @@ def validate_stock_reservation(self): if self.allow_negative_stock: frappe.throw( _("As {0} is enabled, you can not enable {1}.").format( - frappe.bold("Allow Negative Stock"), frappe.bold("Stock Reservation") + frappe.bold(_("Allow Negative Stock")), frappe.bold(_("Stock Reservation")) ) ) @@ -207,7 +208,7 @@ def validate_stock_reservation(self): if bin_with_negative_stock: frappe.throw( _("As there are negative stock, you can not enable {0}.").format( - frappe.bold("Stock Reservation") + frappe.bold(_("Stock Reservation")) ) ) @@ -221,7 +222,7 @@ def validate_stock_reservation(self): if has_reserved_stock: frappe.throw( _("As there are reserved stock, you cannot disable {0}.").format( - frappe.bold("Stock Reservation") + frappe.bold(_("Stock Reservation")) ) ) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 6485c5f60228..d5d492a2c93c 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -10,7 +10,7 @@ from frappe.model.meta import get_field_precision from frappe.model.utils import get_fetch_values from frappe.query_builder.functions import IfNull, Sum -from frappe.utils import add_days, add_months, cint, cstr, flt, getdate +from frappe.utils import add_days, add_months, cint, cstr, flt, getdate, parse_json from erpnext import get_company_currency from erpnext.accounts.doctype.pricing_rule.pricing_rule import ( @@ -807,14 +807,10 @@ def get_price_list_rate(args, item_doc, out=None): if price_list_rate is None or frappe.db.get_single_value( "Stock Settings", "update_existing_price_list_rate" ): - if args.get("is_internal_supplier") or args.get("is_internal_customer"): - return out + insert_item_price(args) - if args.price_list and args.rate: - insert_item_price(args) - - if not price_list_rate: - return out + if price_list_rate is None: + return out out.price_list_rate = flt(price_list_rate) * flt(args.plc_conversion_rate) / flt(args.conversion_rate) @@ -835,6 +831,14 @@ def get_price_list_rate(args, item_doc, out=None): def insert_item_price(args): """Insert Item Price if Price List and Price List Rate are specified and currency is the same""" + if ( + not args.price_list + or not args.rate + or args.get("is_internal_supplier") + or args.get("is_internal_customer") + ): + return + if frappe.db.get_value("Price List", args.price_list, "currency", cache=True) == args.currency and cint( frappe.db.get_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing") ): @@ -885,7 +889,7 @@ def insert_item_price(args): ) -def get_item_price(args, item_code, ignore_party=False): +def get_item_price(args, item_code, ignore_party=False, force_batch_no=False) -> list[dict]: """ Get name, price_list_rate from Item Price based on conditions Check if the desired qty is within the increment of the packing list. @@ -902,13 +906,17 @@ def get_item_price(args, item_code, ignore_party=False): (ip.item_code == item_code) & (ip.price_list == args.get("price_list")) & (IfNull(ip.uom, "").isin(["", args.get("uom")])) - & (IfNull(ip.batch_no, "").isin(["", args.get("batch_no")])) ) .orderby(ip.valid_from, order=frappe.qb.desc) .orderby(IfNull(ip.batch_no, ""), order=frappe.qb.desc) .orderby(ip.uom, order=frappe.qb.desc) ) + if force_batch_no: + query = query.where(ip.batch_no == args.get("batch_no")) + else: + query = query.where(IfNull(ip.batch_no, "").isin(["", args.get("batch_no")])) + if not ignore_party: if args.get("customer"): query = query.where(ip.customer == args.get("customer")) @@ -926,6 +934,22 @@ def get_item_price(args, item_code, ignore_party=False): return query.run() +@frappe.whitelist() +def get_batch_based_item_price(params, item_code) -> float: + if isinstance(params, str): + params = parse_json(params) + + item_price = get_item_price(params, item_code, force_batch_no=True) + + if not item_price: + item_price = get_item_price(params, item_code, ignore_party=True, force_batch_no=True) + + if item_price and item_price[0][2] == params.get("uom"): + return item_price[0][1] + + return 0.0 + + def get_price_list_rate_for(args, item_code): """ :param customer: link to Customer DocType diff --git a/erpnext/stock/number_card/total_warehouses/total_warehouses.json b/erpnext/stock/number_card/total_warehouses/total_warehouses.json index ab0836a3afe0..8eadfac48d1c 100644 --- a/erpnext/stock/number_card/total_warehouses/total_warehouses.json +++ b/erpnext/stock/number_card/total_warehouses/total_warehouses.json @@ -3,6 +3,7 @@ "docstatus": 0, "doctype": "Number Card", "document_type": "Warehouse", + "dynamic_filters_json": "[[\"Warehouse\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]", "filters_json": "[[\"Warehouse\",\"disabled\",\"=\",0]]", "function": "Count", "idx": 0, diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py index 7bc8b71917c0..570dc3a34050 100644 --- a/erpnext/stock/reorder_item.py +++ b/erpnext/stock/reorder_item.py @@ -98,6 +98,7 @@ def add_to_material_request(**kwargs): "description": d.description, "stock_uom": d.stock_uom, "purchase_uom": d.purchase_uom, + "lead_time_days": d.lead_time_days, } ), ) @@ -129,6 +130,7 @@ def get_items_for_reorder() -> dict[str, list]: item_table.brand, item_table.variant_of, item_table.has_variants, + item_table.lead_time_days, ) .where( (item_table.disabled == 0) @@ -354,9 +356,14 @@ def get_email_list(company): def get_comapny_wise_users(company): + companies = [company] + + if parent_company := frappe.db.get_value("Company", company, "parent_company"): + companies.append(parent_company) + users = frappe.get_all( "User Permission", - filters={"allow": "Company", "for_value": company, "apply_to_all_doctypes": 1}, + filters={"allow": "Company", "for_value": ("in", companies), "apply_to_all_doctypes": 1}, fields=["user"], ) diff --git a/erpnext/stock/report/available_batch_report/available_batch_report.js b/erpnext/stock/report/available_batch_report/available_batch_report.js index 011f7e09ca2e..a13e4ca82f76 100644 --- a/erpnext/stock/report/available_batch_report/available_batch_report.js +++ b/erpnext/stock/report/available_batch_report/available_batch_report.js @@ -17,7 +17,7 @@ frappe.query_reports["Available Batch Report"] = { fieldtype: "Date", width: "80", reqd: 1, - default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + default: frappe.datetime.get_today(), }, { fieldname: "item_code", diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index 822da13cc722..0bb9d40581a6 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -168,7 +168,7 @@ def get_stock_ledger_entries_for_batch_bundle(filters): & (sle.has_batch_no == 1) & (sle.posting_date <= filters["to_date"]) ) - .groupby(batch_package.batch_no, batch_package.warehouse) + .groupby(sle.voucher_no, batch_package.batch_no, batch_package.warehouse) .orderby(sle.item_code, sle.warehouse) ) diff --git a/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py index 15d9a12bc65e..486828af1cc1 100644 --- a/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py +++ b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py @@ -106,8 +106,6 @@ def get_columns(filters, data): { "label": _("Voucher Type"), "fieldname": "voucher_type", - "fieldtype": "Link", - "options": "DocType", "width": 120, }, { diff --git a/erpnext/stock/report/serial_no_service_contract_expiry/serial_no_service_contract_expiry.json b/erpnext/stock/report/serial_no_service_contract_expiry/serial_no_service_contract_expiry.json index 6d20719f8eb8..75e2fac98fd8 100644 --- a/erpnext/stock/report/serial_no_service_contract_expiry/serial_no_service_contract_expiry.json +++ b/erpnext/stock/report/serial_no_service_contract_expiry/serial_no_service_contract_expiry.json @@ -1,30 +1,33 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2013-01-14 10:52:58", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "json": "{\"add_total_row\": 0, \"sort_by\": \"Serial No.modified\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [[\"Serial No\", \"delivery_document_type\", \"in\", [\"Delivery Note\", \"Sales Invoice\"]], [\"Serial No\", \"warehouse\", \"=\", \"\"]], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"amc_expiry_date\", \"Serial No\"], [\"maintenance_status\", \"Serial No\"], [\"delivery_document_no\", \"Serial No\"], [\"customer\", \"Serial No\"], [\"customer_name\", \"Serial No\"], [\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"]]}", - "modified": "2017-02-24 20:02:00.706889", - "modified_by": "Administrator", - "module": "Stock", - "name": "Serial No Service Contract Expiry", - "owner": "Administrator", - "ref_doctype": "Serial No", - "report_name": "Serial No Service Contract Expiry", - "report_type": "Report Builder", + "add_total_row": 0, + "columns": [], + "creation": "2013-01-14 10:52:58", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 3, + "is_standard": "Yes", + "json": "{\"add_total_row\": 0, \"sort_by\": \"Serial No.modified\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [[\"Serial No\", \"warehouse\", \"=\", \"\"]], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"amc_expiry_date\", \"Serial No\"], [\"maintenance_status\", \"Serial No\"],[\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"]]}", + "letterhead": null, + "modified": "2024-09-26 13:07:23.451182", + "modified_by": "Administrator", + "module": "Stock", + "name": "Serial No Service Contract Expiry", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Serial No", + "report_name": "Serial No Service Contract Expiry", + "report_type": "Report Builder", "roles": [ { "role": "Item Manager" - }, + }, { "role": "Stock Manager" - }, + }, { "role": "Stock User" } ] -} \ No newline at end of file +} diff --git a/erpnext/stock/report/serial_no_status/serial_no_status.json b/erpnext/stock/report/serial_no_status/serial_no_status.json index 8f03567c8529..d74c2087f729 100644 --- a/erpnext/stock/report/serial_no_status/serial_no_status.json +++ b/erpnext/stock/report/serial_no_status/serial_no_status.json @@ -1,30 +1,33 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2013-01-14 10:52:58", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 4, - "is_standard": "Yes", - "json": "{\"add_total_row\": 0, \"sort_by\": \"Serial No.name\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"warehouse\", \"Serial No\"], [\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"], [\"purchase_document_type\", \"Serial No\"], [\"purchase_document_no\", \"Serial No\"], [\"purchase_date\", \"Serial No\"], [\"customer\", \"Serial No\"], [\"customer_name\", \"Serial No\"], [\"purchase_rate\", \"Serial No\"], [\"delivery_document_type\", \"Serial No\"], [\"delivery_document_no\", \"Serial No\"], [\"delivery_date\", \"Serial No\"], [\"supplier\", \"Serial No\"], [\"supplier_name\", \"Serial No\"]]}", - "modified": "2017-02-24 19:54:21.392265", - "modified_by": "Administrator", - "module": "Stock", - "name": "Serial No Status", - "owner": "Administrator", - "ref_doctype": "Serial No", - "report_name": "Serial No Status", - "report_type": "Report Builder", + "add_total_row": 0, + "columns": [], + "creation": "2013-01-14 10:52:58", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 4, + "is_standard": "Yes", + "json": "{\"add_total_row\": 0, \"sort_by\": \"Serial No.name\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"warehouse\", \"Serial No\"], [\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"],[\"purchase_document_no\", \"Serial No\"]]}", + "letterhead": null, + "modified": "2024-09-26 13:10:52.693648", + "modified_by": "Administrator", + "module": "Stock", + "name": "Serial No Status", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Serial No", + "report_name": "Serial No Status", + "report_type": "Report Builder", "roles": [ { "role": "Item Manager" - }, + }, { "role": "Stock Manager" - }, + }, { "role": "Stock User" } ] -} \ No newline at end of file +} diff --git a/erpnext/stock/report/serial_no_warranty_expiry/serial_no_warranty_expiry.json b/erpnext/stock/report/serial_no_warranty_expiry/serial_no_warranty_expiry.json index ef345f452163..75e2fac98fd8 100644 --- a/erpnext/stock/report/serial_no_warranty_expiry/serial_no_warranty_expiry.json +++ b/erpnext/stock/report/serial_no_warranty_expiry/serial_no_warranty_expiry.json @@ -1,30 +1,33 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2013-01-14 10:52:58", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "json": "{\"add_total_row\": 0, \"sort_by\": \"Serial No.modified\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [[\"Serial No\", \"delivery_document_type\", \"in\", [\"Delivery Note\", \"Sales Invoice\"]], [\"Serial No\", \"warehouse\", \"=\", \"\"]], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"warranty_expiry_date\", \"Serial No\"], [\"warranty_period\", \"Serial No\"], [\"maintenance_status\", \"Serial No\"], [\"purchase_document_no\", \"Serial No\"], [\"purchase_date\", \"Serial No\"], [\"supplier\", \"Serial No\"], [\"supplier_name\", \"Serial No\"], [\"delivery_document_no\", \"Serial No\"], [\"delivery_date\", \"Serial No\"], [\"customer\", \"Serial No\"], [\"customer_name\", \"Serial No\"], [\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"]]}", - "modified": "2017-02-24 20:01:53.097456", - "modified_by": "Administrator", - "module": "Stock", - "name": "Serial No Warranty Expiry", - "owner": "Administrator", - "ref_doctype": "Serial No", - "report_name": "Serial No Warranty Expiry", - "report_type": "Report Builder", + "add_total_row": 0, + "columns": [], + "creation": "2013-01-14 10:52:58", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 3, + "is_standard": "Yes", + "json": "{\"add_total_row\": 0, \"sort_by\": \"Serial No.modified\", \"sort_order\": \"desc\", \"sort_by_next\": null, \"filters\": [[\"Serial No\", \"warehouse\", \"=\", \"\"]], \"sort_order_next\": \"desc\", \"columns\": [[\"name\", \"Serial No\"], [\"item_code\", \"Serial No\"], [\"amc_expiry_date\", \"Serial No\"], [\"maintenance_status\", \"Serial No\"],[\"item_name\", \"Serial No\"], [\"description\", \"Serial No\"], [\"item_group\", \"Serial No\"], [\"brand\", \"Serial No\"]]}", + "letterhead": null, + "modified": "2024-09-26 13:07:23.451182", + "modified_by": "Administrator", + "module": "Stock", + "name": "Serial No Service Contract Expiry", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Serial No", + "report_name": "Serial No Service Contract Expiry", + "report_type": "Report Builder", "roles": [ { "role": "Item Manager" - }, + }, { "role": "Stock Manager" - }, + }, { "role": "Stock User" } ] -} \ No newline at end of file +} diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.js b/erpnext/stock/report/stock_ageing/stock_ageing.js index 578869b6e932..a3ebf14571d2 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.js +++ b/erpnext/stock/report/stock_ageing/stock_ageing.js @@ -54,25 +54,10 @@ frappe.query_reports["Stock Ageing"] = { options: "Brand", }, { - fieldname: "range1", - label: __("Ageing Range 1"), - fieldtype: "Int", - default: "30", - reqd: 1, - }, - { - fieldname: "range2", - label: __("Ageing Range 2"), - fieldtype: "Int", - default: "60", - reqd: 1, - }, - { - fieldname: "range3", - label: __("Ageing Range 3"), - fieldtype: "Int", - default: "90", - reqd: 1, + fieldname: "range", + label: __("Ageing Range"), + fieldtype: "Data", + default: "30, 60, 90", }, { fieldname: "show_warehouse_wise_stock", diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 09af3f224a32..c7e0af6cd383 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -16,6 +16,7 @@ def execute(filters: Filters = None) -> tuple: to_date = filters["to_date"] + filters.ranges = [num.strip() for num in filters.range.split(",") if num.strip().isdigit()] columns = get_columns(filters) item_details = FIFOSlots(filters).generate() @@ -48,7 +49,7 @@ def format_report_data(filters: Filters, item_details: dict, to_date: str) -> li average_age = get_average_age(fifo_queue, to_date) earliest_age = date_diff(to_date, fifo_queue[0][1]) latest_age = date_diff(to_date, fifo_queue[-1][1]) - range1, range2, range3, above_range3 = get_range_age(filters, fifo_queue, to_date, item_dict) + range_values = get_range_age(filters, fifo_queue, to_date, item_dict) row = [details.name, details.item_name, details.description, details.item_group, details.brand] @@ -59,10 +60,7 @@ def format_report_data(filters: Filters, item_details: dict, to_date: str) -> li [ flt(item_dict.get("total_qty"), precision), average_age, - range1, - range2, - range3, - above_range3, + *range_values, earliest_age, latest_age, details.stock_uom, @@ -89,25 +87,22 @@ def get_average_age(fifo_queue: list, to_date: str) -> float: return flt(age_qty / total_qty, 2) if total_qty else 0.0 -def get_range_age(filters: Filters, fifo_queue: list, to_date: str, item_dict: dict) -> tuple: +def get_range_age(filters: Filters, fifo_queue: list, to_date: str, item_dict: dict) -> list: precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True)) - - range1 = range2 = range3 = above_range3 = 0.0 + range_values = [0.0] * (len(filters.ranges) + 1) for item in fifo_queue: age = flt(date_diff(to_date, item[1])) qty = flt(item[0]) if not item_dict["has_serial_no"] else 1.0 - if age <= flt(filters.range1): - range1 = flt(range1 + qty, precision) - elif age <= flt(filters.range2): - range2 = flt(range2 + qty, precision) - elif age <= flt(filters.range3): - range3 = flt(range3 + qty, precision) + for i, age_limit in enumerate(filters.ranges): + if age <= flt(age_limit): + range_values[i] = flt(range_values[i] + qty, precision) + break else: - above_range3 = flt(above_range3 + qty, precision) + range_values[-1] = flt(range_values[-1] + qty, precision) - return range1, range2, range3, above_range3 + return range_values def get_columns(filters: Filters) -> list[dict]: @@ -193,12 +188,14 @@ def get_chart_data(data: list, filters: Filters) -> dict: def setup_ageing_columns(filters: Filters, range_columns: list): - ranges = [ - f"0 - {filters['range1']}", - f"{cint(filters['range1']) + 1} - {cint(filters['range2'])}", - f"{cint(filters['range2']) + 1} - {cint(filters['range3'])}", - _("{0} - Above").format(cint(filters["range3"]) + 1), - ] + prev_range_value = 0 + ranges = [] + for range in filters.ranges: + ranges.append(f"{prev_range_value} - {range}") + prev_range_value = cint(range) + 1 + + ranges.append(f"{prev_range_value} - Above") + for i, label in enumerate(ranges): fieldname = "range" + str(i + 1) add_column(range_columns, label=_("Age ({0})").format(label), fieldname=fieldname) diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index fb3636062338..c0c007e5015a 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -9,9 +9,7 @@ class TestStockAgeing(FrappeTestCase): def setUp(self) -> None: - self.filters = frappe._dict( - company="_Test Company", to_date="2021-12-10", range1=30, range2=60, range3=90 - ) + self.filters = frappe._dict(company="_Test Company", to_date="2021-12-10", ranges=["30", "60", "90"]) def test_normal_inward_outward_queue(self): "Reference: Case 1 in stock_ageing_fifo_logic.md (same wh)" diff --git a/erpnext/stock/report/stock_balance/stock_balance.js b/erpnext/stock/report/stock_balance/stock_balance.js index d80261895aa5..1d86634fd951 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.js +++ b/erpnext/stock/report/stock_balance/stock_balance.js @@ -107,6 +107,12 @@ frappe.query_reports["Stock Balance"] = { fieldtype: "Check", default: 0, }, + { + fieldname: "show_dimension_wise_stock", + label: __("Show Dimension Wise Stock"), + fieldtype: "Check", + default: 0, + }, ], formatter: function (value, row, column, data, default_formatter) { diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 2694ba03c8be..8ea07338cf1b 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -252,7 +252,10 @@ def get_group_by_key(self, row) -> tuple: group_by_key = [row.company, row.item_code, row.warehouse] for fieldname in self.inventory_dimensions: - if self.filters.get(fieldname): + if not row.get(fieldname): + continue + + if self.filters.get(fieldname) or self.filters.get("show_dimension_wise_stock"): group_by_key.append(row.get(fieldname)) return tuple(group_by_key) diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js index 1b05d3a65e4b..df65654e36bd 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js +++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js @@ -32,6 +32,12 @@ frappe.query_reports["Stock Ledger Invariant Check"] = { mandatory: 1, options: "Warehouse", }, + { + fieldname: "show_incorrect_entries", + fieldtype: "Check", + label: "Show Incorrect Entries", + default: 0, + }, ], formatter(value, row, column, data, default_formatter) { diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py index fb392f7e36a5..5cf653e38517 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py +++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py @@ -5,7 +5,7 @@ import frappe from frappe import _ -from frappe.utils import get_link_to_form, parse_json +from frappe.utils import cint, flt, get_link_to_form, parse_json SLE_FIELDS = ( "name", @@ -36,7 +36,7 @@ def execute(filters=None): def get_data(filters): sles = get_stock_ledger_entries(filters) - return add_invariant_check_fields(sles) + return add_invariant_check_fields(sles, filters) def get_stock_ledger_entries(filters): @@ -48,11 +48,14 @@ def get_stock_ledger_entries(filters): ) -def add_invariant_check_fields(sles): +def add_invariant_check_fields(sles, filters): balance_qty = 0.0 balance_stock_value = 0.0 + + incorrect_idx = 0 + precision = frappe.get_precision("Stock Ledger Entry", "actual_qty") for idx, sle in enumerate(sles): - queue = json.loads(sle.stock_queue) + queue = json.loads(sle.stock_queue) if sle.stock_queue else [] fifo_qty = 0.0 fifo_value = 0.0 @@ -95,6 +98,12 @@ def add_invariant_check_fields(sles): ) sle.diff_value_diff = sle.stock_value_from_diff - sle.stock_value + if not incorrect_idx and filters.get("show_incorrect_entries"): + if is_sle_has_correct_data(sle, precision): + continue + else: + incorrect_idx = idx + if idx > 0: sle.fifo_stock_diff = sle.fifo_stock_value - sles[idx - 1].fifo_stock_value sle.fifo_difference_diff = sle.fifo_stock_diff - sle.stock_value_difference @@ -104,9 +113,23 @@ def add_invariant_check_fields(sles): "Batch", sle.batch_no, "use_batchwise_valuation", cache=True ) + if filters.get("show_incorrect_entries"): + if incorrect_idx > 0: + sles = sles[cint(incorrect_idx) - 1 :] + + return [] + return sles +def is_sle_has_correct_data(sle, precision): + if flt(sle.difference_in_qty, precision) != 0.0 or flt(sle.diff_value_diff, precision) != 0: + print(flt(sle.difference_in_qty, precision), flt(sle.diff_value_diff, precision)) + return False + + return True + + def get_columns(): return [ { diff --git a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js index 07e7b59b5146..5dfb6627662d 100644 --- a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js +++ b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js @@ -47,7 +47,23 @@ frappe.query_reports["Stock Ledger Variance"] = { fieldname: "difference_in", fieldtype: "Select", label: __("Difference In"), - options: ["", "Qty", "Value", "Valuation"], + options: [ + { + // Check "Stock Ledger Invariant Check" report with A - B column + label: __("Quantity (A - B)"), + value: "Qty", + }, + { + // Check "Stock Ledger Invariant Check" report with G - D column + label: __("Value (G - D)"), + value: "Value", + }, + { + // Check "Stock Ledger Invariant Check" report with I - K column + label: __("Valuation (I - K)"), + value: "Valuation", + }, + ], }, { fieldname: "include_disabled", diff --git a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py index 0b7e551c86f2..808afadd05ae 100644 --- a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py +++ b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py @@ -1,6 +1,8 @@ # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +import json + import frappe from frappe import _ from frappe.utils import cint, flt @@ -270,12 +272,16 @@ def has_difference(row, precision, difference_in, valuation_method): value_diff = flt(row.diff_value_diff, precision) valuation_diff = flt(row.valuation_diff, precision) else: - qty_diff = flt(row.difference_in_qty, precision) or flt(row.fifo_qty_diff, precision) - value_diff = ( - flt(row.diff_value_diff, precision) - or flt(row.fifo_value_diff, precision) - or flt(row.fifo_difference_diff, precision) - ) + qty_diff = flt(row.difference_in_qty, precision) + value_diff = flt(row.diff_value_diff, precision) + + if row.stock_queue and json.loads(row.stock_queue): + value_diff = value_diff or ( + flt(row.fifo_value_diff, precision) or flt(row.fifo_difference_diff, precision) + ) + + qty_diff = qty_diff or flt(row.fifo_qty_diff, precision) + valuation_diff = flt(row.valuation_diff, precision) or flt(row.fifo_valuation_diff, precision) if difference_in == "Qty" and qty_diff: diff --git a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py index 743656c6472c..9b4520064d69 100644 --- a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py +++ b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py @@ -297,6 +297,7 @@ def get_item_map(item_code, include_uom): if include_uom: ucd = frappe.qb.DocType("UOM Conversion Detail") + query = query.select(ucd.conversion_factor) query = query.left_join(ucd).on((ucd.parent == item.name) & (ucd.uom == include_uom)) items = query.run(as_dict=True) diff --git a/erpnext/stock/report/test_reports.py b/erpnext/stock/report/test_reports.py index 74c6afa204b1..ad2b46b393fa 100644 --- a/erpnext/stock/report/test_reports.py +++ b/erpnext/stock/report/test_reports.py @@ -1,6 +1,7 @@ import unittest import frappe +from frappe.utils.make_random import get_random from erpnext.tests.utils import ReportFilters, ReportName, execute_script_report @@ -11,7 +12,7 @@ } -batch = frappe.db.get_value("Batch", fieldname=["name"], as_dict=True, order_by="creation desc") +batch = get_random("Batch") REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [ ("Stock Ledger", {"_optional": True}), @@ -62,7 +63,7 @@ ("Item Prices", {"items": "Enabled Items only"}), ("Delayed Item Report", {"based_on": "Sales Invoice"}), ("Delayed Item Report", {"based_on": "Delivery Note"}), - ("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}), + ("Stock Ageing", {"range": "30, 60, 90", "_optional": True}), ("Stock Ledger Invariant Check", {"warehouse": "_Test Warehouse - _TC", "item": "_Test Item"}), ("FIFO Queue vs Qty After Transaction Comparison", {"warehouse": "_Test Warehouse - _TC"}), ("FIFO Queue vs Qty After Transaction Comparison", {"item_group": "All Item Groups"}), diff --git a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.js b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.js index 137c786e44b5..ac1ecfff5303 100644 --- a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.js +++ b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.js @@ -3,6 +3,15 @@ frappe.query_reports["Warehouse wise Item Balance Age and Value"] = { filters: [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + width: "80", + options: "Company", + reqd: 1, + default: frappe.defaults.get_user_default("Company"), + }, { fieldname: "from_date", label: __("From Date"), @@ -39,6 +48,12 @@ frappe.query_reports["Warehouse wise Item Balance Age and Value"] = { fieldtype: "Link", width: "80", options: "Warehouse", + get_query: function () { + const company = frappe.query_report.get_filter_value("company"); + return { + filters: { company: company }, + }; + }, }, { fieldname: "filter_total_zero_qty", diff --git a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py index 0df8b6ddb48b..68caff403563 100644 --- a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py +++ b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py @@ -109,8 +109,6 @@ def validate_filters(filters): sle_count = flt(frappe.qb.from_("Stock Ledger Entry").select(Count("name")).run()[0][0]) if sle_count > 500000: frappe.throw(_("Please set filter based on Item or Warehouse")) - if not filters.get("company"): - filters["company"] = frappe.defaults.get_user_default("Company") def get_warehouse_list(filters): diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 71e664dd0668..05b3536c57ba 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -124,7 +124,7 @@ def validate_actual_qty(self, sn_doc): "Outward": self.sle.actual_qty < 0, }.get(sn_doc.type_of_transaction) - if not condition: + if not condition and self.sle.actual_qty: correct_type = "Inward" if sn_doc.type_of_transaction == "Inward": correct_type = "Outward" @@ -133,7 +133,7 @@ def validate_actual_qty(self, sn_doc): frappe.throw(_(msg), title=_("Incorrect Type of Transaction")) precision = sn_doc.precision("total_qty") - if flt(sn_doc.total_qty, precision) != flt(self.sle.actual_qty, precision): + if self.sle.actual_qty and flt(sn_doc.total_qty, precision) != flt(self.sle.actual_qty, precision): msg = f"Total qty {flt(sn_doc.total_qty, precision)} of Serial and Batch Bundle {link} is not equal to Actual Qty {flt(self.sle.actual_qty, precision)} in the {self.sle.voucher_type} {self.sle.voucher_no}" frappe.throw(_(msg)) @@ -183,7 +183,7 @@ def set_serial_and_batch_bundle(self, sn_doc): } if self.sle.actual_qty < 0 and self.is_material_transfer(): - values_to_update["valuation_rate"] = sn_doc.avg_rate + values_to_update["valuation_rate"] = flt(sn_doc.avg_rate) if not frappe.db.get_single_value( "Stock Settings", "do_not_update_serial_batch_on_creation_of_auto_bundle" @@ -288,7 +288,7 @@ def post_process(self): "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "docstatus" ) - if docstatus != 1: + if docstatus == 0: self.submit_serial_and_batch_bundle() if self.item_details.has_serial_no == 1: @@ -311,7 +311,9 @@ def cancel_serial_and_batch_bundle(self): if self.is_pos_transaction(): return - frappe.get_cached_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle).cancel() + doc = frappe.get_cached_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle) + if doc.docstatus == 1: + doc.cancel() def is_pos_transaction(self): if ( @@ -476,7 +478,7 @@ def get_serial_or_batch_nos(bundle): html = "" for d in data: if d.serial_no: - html += f"" + html += f"" else: html += f"" @@ -506,7 +508,7 @@ def calculate_stock_value_change(self): serial_nos = self.get_serial_nos() for serial_no in serial_nos: incoming_rate = self.get_incoming_rate_from_bundle(serial_no) - if not incoming_rate: + if incoming_rate is None: continue self.stock_value_change += incoming_rate @@ -551,7 +553,7 @@ def get_incoming_rate_from_bundle(self, serial_no) -> float: query = query.where(timestamp_condition) incoming_rate = query.run() - return flt(incoming_rate[0][0]) if incoming_rate else 0.0 + return flt(incoming_rate[0][0]) if incoming_rate else None def get_serial_nos(self): if self.sle.get("serial_nos"): @@ -984,7 +986,7 @@ def validate_qty(self, doc): required_qty = flt(abs(self.actual_qty), precision) if required_qty - total_qty > 0: - msg = f"For the item {bold(doc.item_code)}, the Avaliable qty {bold(total_qty)} is less than the Required Qty {bold(required_qty)} in the warehouse {bold(doc.warehouse)}. Please add sufficient qty in the warehouse." + msg = f"For the item {bold(doc.item_code)}, the Available qty {bold(total_qty)} is less than the Required Qty {bold(required_qty)} in the warehouse {bold(doc.warehouse)}. Please add sufficient qty in the warehouse." frappe.throw(msg, title=_("Insufficient Stock")) def set_auto_serial_batch_entries_for_outward(self): @@ -1088,6 +1090,8 @@ def make_serial_nos(self, serial_nos): frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details)) def set_serial_batch_entries(self, doc): + incoming_rate = self.get("incoming_rate") + if self.get("serial_nos"): serial_no_wise_batch = frappe._dict({}) if self.has_batch_no: @@ -1095,30 +1099,54 @@ def set_serial_batch_entries(self, doc): qty = -1 if self.type_of_transaction == "Outward" else 1 for serial_no in self.serial_nos: + if self.get("serial_nos_valuation"): + incoming_rate = self.get("serial_nos_valuation").get(serial_no) + doc.append( "entries", { "serial_no": serial_no, "qty": qty, "batch_no": serial_no_wise_batch.get(serial_no) or self.get("batch_no"), - "incoming_rate": self.get("incoming_rate"), + "incoming_rate": incoming_rate, }, ) elif self.get("batches"): for batch_no, batch_qty in self.batches.items(): + if self.get("batches_valuation"): + incoming_rate = self.get("batches_valuation").get(batch_no) + doc.append( "entries", { "batch_no": batch_no, "qty": batch_qty * (-1 if self.type_of_transaction == "Outward" else 1), - "incoming_rate": self.get("incoming_rate"), + "incoming_rate": incoming_rate, }, ) def create_batch(self): from erpnext.stock.doctype.batch.batch import make_batch + if self.is_rejected: + bundle = frappe.db.get_value( + "Serial and Batch Bundle", + { + "voucher_no": self.voucher_no, + "voucher_type": self.voucher_type, + "voucher_detail_no": self.voucher_detail_no, + "is_rejected": 0, + "docstatus": 1, + "is_cancelled": 0, + }, + "name", + ) + + if bundle: + if batch_no := frappe.db.get_value("Serial and Batch Entry", {"parent": bundle}, "batch_no"): + return batch_no + return make_batch( frappe._dict( { diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 693481dfba8f..26e7af150b9c 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -623,9 +623,21 @@ def build(self): if sle.dependant_sle_voucher_detail_no: entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle) + if self.has_stock_reco_with_serial_batch(sle): + break + if self.exceptions: self.raise_exceptions() + def has_stock_reco_with_serial_batch(self, sle): + if ( + sle.vocher_type == "Stock Reconciliation" + and frappe.db.get_value(sle.voucher_type, sle.voucher_no, "set_posting_time") == 1 + ): + return not (sle.batch_no or sle.serial_no or sle.serial_and_batch_bundle) + + return False + def process_sle_against_current_timestamp(self): sl_entries = self.get_sle_against_current_voucher() for sle in sl_entries: @@ -647,6 +659,7 @@ def get_sle_against_current_voucher(self): and ( posting_datetime = %(posting_datetime)s ) + and creation = %(creation)s order by creation ASC for update @@ -864,7 +877,7 @@ def process_sle(self, sle): sle.stock_value = self.wh_data.stock_value sle.stock_queue = json.dumps(self.wh_data.stock_queue) - if not sle.is_adjustment_entry or not self.args.get("sle_id"): + if not sle.is_adjustment_entry: sle.stock_value_difference = stock_value_difference sle.doctype = "Stock Ledger Entry" @@ -1034,7 +1047,7 @@ def get_incoming_outgoing_rate_from_transaction(self, sle): rate = 0 # Material Transfer, Repack, Manufacturing if sle.voucher_type == "Stock Entry": - self.recalculate_amounts_in_stock_entry(sle.voucher_no) + self.recalculate_amounts_in_stock_entry(sle.voucher_no, sle.voucher_detail_no) rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate") # Sales and Purchase Return elif sle.voucher_type in ( @@ -1073,6 +1086,15 @@ def get_incoming_outgoing_rate_from_transaction(self, sle): } ) + if not rate and sle.voucher_type in ["Delivery Note", "Sales Invoice"]: + rate = get_rate_for_return( + sle.voucher_type, + sle.voucher_no, + sle.item_code, + voucher_detail_no=sle.voucher_detail_no, + sle=sle, + ) + else: rate = get_rate_for_return( sle.voucher_type, @@ -1154,14 +1176,16 @@ def update_rate_on_stock_entry(self, sle, outgoing_rate): # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount if not sle.dependant_sle_voucher_detail_no: - self.recalculate_amounts_in_stock_entry(sle.voucher_no) + self.recalculate_amounts_in_stock_entry(sle.voucher_no, sle.voucher_detail_no) - def recalculate_amounts_in_stock_entry(self, voucher_no): + def recalculate_amounts_in_stock_entry(self, voucher_no, voucher_detail_no): stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True) stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False) stock_entry.db_update() for d in stock_entry.items: - d.db_update() + # Update only the row that matches the voucher_detail_no or the row containing the FG/Scrap Item. + if d.name == voucher_detail_no or (not d.s_warehouse and d.t_warehouse): + d.db_update() def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate): # Update item's incoming rate on transaction @@ -1526,7 +1550,12 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc voucher_no = args.get("voucher_no") voucher_condition = f"and voucher_no != '{voucher_no}'" - sle = frappe.db.sql( + elif args.get("creation") and args.get("sle_id"): + creation = args.get("creation") + operator = "<=" + voucher_condition = f"and creation < '{creation}'" + + sle = frappe.db.sql( # nosemgrep f""" select *, posting_datetime as "timestamp" from `tabStock Ledger Entry` @@ -1537,7 +1566,7 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc and ( posting_datetime {operator} %(posting_datetime)s ) - order by posting_datetime desc, creation desc + order by posting_date desc, posting_time desc, creation desc limit 1 for update""", { @@ -1623,6 +1652,7 @@ def get_stock_ledger_entries( if extra_cond: conditions += f"{extra_cond}" + # nosemgrep return frappe.db.sql( """ select *, posting_datetime as "timestamp" @@ -1630,7 +1660,7 @@ def get_stock_ledger_entries( where item_code = %(item_code)s and is_cancelled = 0 {conditions} - order by posting_datetime {order}, creation {order} + order by posting_date {order}, posting_time {order}, creation {order} {limit} {for_update}""".format( conditions=conditions, limit=limit or "", @@ -1724,6 +1754,9 @@ def get_valuation_rate( # Get moving average rate of a specific batch number if warehouse and serial_and_batch_bundle: + sabb = frappe.db.get_value( + "Serial and Batch Bundle", serial_and_batch_bundle, ["posting_date", "posting_time"], as_dict=True + ) batch_obj = BatchNoValuation( sle=frappe._dict( { @@ -1731,6 +1764,8 @@ def get_valuation_rate( "warehouse": warehouse, "actual_qty": -1, "serial_and_batch_bundle": serial_and_batch_bundle, + "posting_date": sabb.posting_date, + "posting_time": sabb.posting_time, } ) ) @@ -1738,7 +1773,7 @@ def get_valuation_rate( return batch_obj.get_incoming_rate() # Get valuation rate from last sle for the same item and warehouse - if last_valuation_rate := frappe.db.sql( + if last_valuation_rate := frappe.db.sql( # nosemgrep """select valuation_rate from `tabStock Ledger Entry` force index (item_warehouse) where @@ -1747,7 +1782,7 @@ def get_valuation_rate( AND valuation_rate >= 0 AND is_cancelled = 0 AND NOT (voucher_no = %s AND voucher_type = %s) - order by posting_datetime desc, name desc limit 1""", + order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type), ): return flt(last_valuation_rate[0][0]) @@ -1818,7 +1853,7 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): detail = next_stock_reco_detail[0] datetime_limit_condition = get_datetime_limit_condition(detail) - frappe.db.sql( + frappe.db.sql( # nosemgrep f""" update `tabStock Ledger Entry` set qty_after_transaction = qty_after_transaction + {qty_shift} @@ -1984,8 +2019,8 @@ def is_negative_with_precision(neg_sle, is_batch=False): return qty_deficit < 0 and abs(qty_deficit) > 0.0001 -def get_future_sle_with_negative_qty(args): - return frappe.db.sql( +def get_future_sle_with_negative_qty(sle_args): + return frappe.db.sql( # nosemgrep """ select qty_after_transaction, posting_date, posting_time, @@ -1998,28 +2033,28 @@ def get_future_sle_with_negative_qty(args): and posting_datetime >= %(posting_datetime)s and is_cancelled = 0 and qty_after_transaction < 0 - order by posting_datetime asc + order by posting_date asc, posting_time asc limit 1 """, - args, + sle_args, as_dict=1, ) -def get_future_sle_with_negative_batch_qty(args): - return frappe.db.sql( +def get_future_sle_with_negative_batch_qty(sle_args): + return frappe.db.sql( # nosemgrep """ with batch_ledger as ( select posting_date, posting_time, posting_datetime, voucher_type, voucher_no, - sum(actual_qty) over (order by posting_datetime, creation) as cumulative_total + sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total from `tabStock Ledger Entry` where item_code = %(item_code)s and warehouse = %(warehouse)s and batch_no=%(batch_no)s and is_cancelled = 0 - order by posting_datetime, creation + order by posting_date, posting_time, creation ) select * from batch_ledger where @@ -2027,7 +2062,7 @@ def get_future_sle_with_negative_batch_qty(args): and posting_datetime >= %(posting_datetime)s limit 1 """, - args, + sle_args, as_dict=1, ) diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json index b8bda8329832..206e3135dfb6 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json @@ -294,7 +294,7 @@ "fieldname": "total", "fieldtype": "Currency", "label": "Total", - "options": "currency", + "options": "Company:company:default_currency", "read_only": 1 }, { @@ -353,6 +353,7 @@ "fieldname": "total_additional_costs", "fieldtype": "Currency", "label": "Total Additional Costs", + "options": "Company:company:default_currency", "print_hide_if_no_value": 1, "read_only": 1 }, @@ -450,21 +451,21 @@ "options": "Project" }, { - "fieldname": "tab_other_info", - "fieldtype": "Tab Break", - "label": "Other Info" + "fieldname": "tab_other_info", + "fieldtype": "Tab Break", + "label": "Other Info" }, { - "fieldname": "tab_connections", - "fieldtype": "Tab Break", - "label": "Connections", - "show_dashboard": 1 + "fieldname": "tab_connections", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 } ], "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2024-01-03 20:56:04.670380", + "modified": "2024-12-06 15:21:49.924146", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Order", @@ -519,4 +520,4 @@ "timeline_field": "supplier", "title_field": "supplier_name", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js index 895ffdbf88c3..f5486bbd59d6 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js @@ -4,7 +4,7 @@ frappe.listview_settings["Subcontracting Order"] = { get_indicator: function (doc) { const status_colors = { - Draft: "grey", + Draft: "red", Open: "orange", "Partially Received": "yellow", Completed: "green", diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py index 0ae07993be86..ac87239e73ed 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py @@ -25,6 +25,7 @@ make_subcontracted_items, set_backflush_based_on, ) +from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( @@ -683,6 +684,28 @@ def test_requested_qty_for_subcontracting_order(self): self.assertEqual(requested_qty, new_requested_qty) + def test_subcontracting_order_rm_required_items_for_precision(self): + item_code = "Subcontracted Item SA9" + raw_materials = ["Subcontracted SRM Item 9"] + if not frappe.db.exists("BOM", {"item": item_code}): + make_bom(item=item_code, raw_materials=raw_materials, rate=100, rm_qty=1.04) + + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 9", + "qty": 1, # 202.0656, + "rate": 100, + "fg_item": "Subcontracted Item SA9", + "fg_item_qty": 202.0656, + }, + ] + + sco = get_subcontracting_order(service_items=service_items) + sco.reload() + + self.assertEqual(sco.supplied_items[0].required_qty, 210.149) + def create_subcontracting_order(**args): args = frappe._dict(args) diff --git a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json index 1ca90c31654d..502a28b3ec62 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json @@ -185,7 +185,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Rate", - "options": "currency", + "options": "Company:company:default_currency", "read_only": 1, "reqd": 1 }, @@ -199,7 +199,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Amount", - "options": "currency", + "options": "Company:company:default_currency", "read_only": 1, "reqd": 1 }, @@ -269,6 +269,7 @@ "fieldname": "service_cost_per_qty", "fieldtype": "Currency", "label": "Service Cost Per Qty", + "options": "Company:company:default_currency", "read_only": 1, "reqd": 1 }, @@ -277,6 +278,7 @@ "fieldname": "additional_cost_per_qty", "fieldtype": "Currency", "label": "Additional Cost Per Qty", + "options": "Company:company:default_currency", "read_only": 1 }, { @@ -284,6 +286,7 @@ "fieldtype": "Currency", "label": "Raw Material Cost Per Qty", "no_copy": 1, + "options": "Company:company:default_currency", "read_only": 1 }, { @@ -384,7 +387,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-30 15:29:43.744618", + "modified": "2024-12-06 15:23:05.252346", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Order Item", @@ -397,4 +400,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py index fcd143c1dd94..7a426f91cb00 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py +++ b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py @@ -26,8 +26,11 @@ class SubcontractingOrderItem(Document): include_exploded_items: DF.Check item_code: DF.Link item_name: DF.Data + job_card: DF.Link | None manufacturer: DF.Link | None manufacturer_part_no: DF.Data | None + material_request: DF.Link | None + material_request_item: DF.Data | None page_break: DF.Check parent: DF.Data parentfield: DF.Data diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js index 83113a223c23..2aaf8a8adcda 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -249,11 +249,15 @@ frappe.ui.form.on("Subcontracting Receipt", { }); frm.set_query("batch_no", "supplied_items", (doc, cdt, cdn) => { - var row = locals[cdt][cdn]; + let row = locals[cdt][cdn]; + let filters = { + item_code: row.rm_item_code, + warehouse: doc.supplier_warehouse, + }; + return { - filters: { - item: row.rm_item_code, - }, + query: "erpnext.controllers.queries.get_batch_no", + filters: filters, }; }); diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json index ad03171f29a2..b8bd95bcbcae 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json @@ -336,7 +336,7 @@ "fieldname": "total", "fieldtype": "Currency", "label": "Total", - "options": "currency", + "options": "Company:company:default_currency", "read_only": 1 }, { @@ -618,6 +618,7 @@ "fieldname": "total_additional_costs", "fieldtype": "Currency", "label": "Total Additional Costs", + "options": "Company:company:default_currency", "print_hide_if_no_value": 1, "read_only": 1 }, @@ -656,27 +657,27 @@ "fieldtype": "Column Break" }, { - "fieldname": "tab_other_info", - "fieldtype": "Tab Break", - "label": "Other Info" + "fieldname": "tab_other_info", + "fieldtype": "Tab Break", + "label": "Other Info" }, { - "collapsible": 1, - "fieldname": "order_status_section", - "fieldtype": "Section Break", - "label": "Order Status" + "collapsible": 1, + "fieldname": "order_status_section", + "fieldtype": "Section Break", + "label": "Order Status" }, { - "fieldname": "tab_connections", - "fieldtype": "Tab Break", - "label": "Connections", - "show_dashboard": 1 + "fieldname": "tab_connections", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 } ], "in_create": 1, "is_submittable": 1, "links": [], - "modified": "2024-05-28 15:02:13.517969", + "modified": "2024-12-06 15:24:38.384232", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 482031671870..37cd43ac1f6c 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -237,9 +237,14 @@ def reset_supplied_items(self): frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on") == "BOM" and self.supplied_items - and not any(item.serial_and_batch_bundle for item in self.supplied_items) ): - self.supplied_items = [] + if not any( + item.serial_and_batch_bundle or item.batch_no or item.serial_no + for item in self.supplied_items + ): + self.supplied_items = [] + else: + self.update_rate_for_supplied_items() @frappe.whitelist() def get_scrap_items(self, recalculate_rate=False): @@ -326,9 +331,12 @@ def set_available_qty_for_consumption(self): supplied_items_details[item.name] = {} for supplied_item in supplied_items: + if supplied_item.rm_item_code not in supplied_items_details[item.name]: + supplied_items_details[item.name][supplied_item.rm_item_code] = 0.0 + supplied_items_details[item.name][ supplied_item.rm_item_code - ] = supplied_item.available_qty + ] += supplied_item.available_qty else: for item in self.get("supplied_items"): item.available_qty_for_consumption = supplied_items_details.get(item.reference_name, {}).get( @@ -682,86 +690,107 @@ def make_purchase_receipt(source_name, target_doc=None, save=False, submit=False else: source_doc = source_name - if not source_doc.is_return: - if not target_doc: - target_doc = frappe.new_doc("Purchase Receipt") - target_doc.is_subcontracted = 1 - target_doc.is_old_subcontracting_flow = 0 + if source_doc.is_return: + return + + po_sr_item_dict = {} + po_name = None + for item in source_doc.items: + if not item.purchase_order: + continue + + if not po_name: + po_name = item.purchase_order + + po_sr_item_dict[item.purchase_order_item] = { + "qty": flt(item.qty), + "rejected_qty": flt(item.rejected_qty), + "warehouse": item.warehouse, + "rejected_warehouse": item.rejected_warehouse, + "subcontracting_receipt_item": item.name, + } + + if not po_name: + frappe.throw( + _("Purchase Order Item reference is missing in Subcontracting Receipt {0}").format( + source_doc.name + ) + ) + + def update_item(obj, target, source_parent): + sr_item_details = po_sr_item_dict.get(obj.name) + ratio = flt(obj.qty) / flt(obj.fg_item_qty) - target_doc = get_mapped_doc( - "Subcontracting Receipt", - source_doc.name, + target.update( { - "Subcontracting Receipt": { - "doctype": "Purchase Receipt", - "field_map": { - "posting_date": "posting_date", - "posting_time": "posting_time", - "name": "subcontracting_receipt", - "supplier_warehouse": "supplier_warehouse", - }, - "field_no_map": ["total_qty", "total"], - }, - }, - target_doc, - ignore_child_tables=True, + "qty": ratio * sr_item_details["qty"], + "rejected_qty": ratio * sr_item_details["rejected_qty"], + "warehouse": sr_item_details["warehouse"], + "rejected_warehouse": sr_item_details["rejected_warehouse"], + "subcontracting_receipt_item": sr_item_details["subcontracting_receipt_item"], + } ) - target_doc.currency = frappe.get_cached_value("Company", target_doc.company, "default_currency") - - po_items_details = {} - for item in source_doc.items: - if item.purchase_order and item.purchase_order_item: - if item.purchase_order not in po_items_details: - po_doc = frappe.get_doc("Purchase Order", item.purchase_order) - po_items_details[item.purchase_order] = { - po_item.name: po_item for po_item in po_doc.items - } - - if po_item := po_items_details[item.purchase_order].get(item.purchase_order_item): - conversion_factor = flt(po_item.qty) / flt(po_item.fg_item_qty) - item_row = { - "item_code": po_item.item_code, - "item_name": po_item.item_name, - "conversion_factor": conversion_factor, - "qty": flt(item.qty) * conversion_factor, - "rejected_qty": flt(item.rejected_qty) * conversion_factor, - "uom": po_item.uom, - "rate": po_item.rate, - "warehouse": item.warehouse, - "rejected_warehouse": item.rejected_warehouse, - "purchase_order": item.purchase_order, - "purchase_order_item": item.purchase_order_item, - "subcontracting_receipt_item": item.name, - "project": po_item.project, - } - target_doc.append("items", item_row) - - if not target_doc.items: - frappe.throw( - _("Purchase Order Item reference is missing in Subcontracting Receipt {0}").format( - source_doc.name - ) - ) + def post_process(source, target): + target.set_missing_values() + target.update( + { + "posting_date": source_doc.posting_date, + "posting_time": source_doc.posting_time, + "subcontracting_receipt": source_doc.name, + "supplier_warehouse": source_doc.supplier_warehouse, + "is_subcontracted": 1, + "is_old_subcontracting_flow": 0, + "currency": frappe.get_cached_value("Company", target.company, "default_currency"), + } + ) - target_doc.set_missing_values() + target_doc = get_mapped_doc( + "Purchase Order", + po_name, + { + "Purchase Order": { + "doctype": "Purchase Receipt", + "field_map": {"supplier_warehouse": "supplier_warehouse"}, + "validation": { + "docstatus": ["=", 1], + }, + }, + "Purchase Order Item": { + "doctype": "Purchase Receipt Item", + "field_map": { + "name": "purchase_order_item", + "parent": "purchase_order", + "bom": "bom", + }, + "postprocess": update_item, + "condition": lambda doc: doc.name in po_sr_item_dict, + }, + "Purchase Taxes and Charges": { + "doctype": "Purchase Taxes and Charges", + "reset_value": True, + "condition": lambda doc: not doc.is_tax_withholding_account, + }, + }, + postprocess=post_process, + ) - if (save or submit) and frappe.has_permission(target_doc.doctype, "create"): - target_doc.save() + if (save or submit) and frappe.has_permission(target_doc.doctype, "create"): + target_doc.save() - if submit and frappe.has_permission(target_doc.doctype, "submit", target_doc): - try: - target_doc.submit() - except Exception as e: - target_doc.add_comment("Comment", _("Submit Action Failed") + "

" + str(e)) + if submit and frappe.has_permission(target_doc.doctype, "submit", target_doc): + try: + target_doc.submit() + except Exception as e: + target_doc.add_comment("Comment", _("Submit Action Failed") + "

" + str(e)) - if notify: - frappe.msgprint( - _("Purchase Receipt {0} created.").format( - get_link_to_form(target_doc.doctype, target_doc.name) - ), - indicator="green", - alert=True, - ) + if notify: + frappe.msgprint( + _("Purchase Receipt {0} created.").format( + get_link_to_form(target_doc.doctype, target_doc.name) + ), + indicator="green", + alert=True, + ) - return target_doc + return target_doc diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_list.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_list.js index be6c0d0b18f3..5f5a6db01e0a 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_list.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_list.js @@ -4,7 +4,7 @@ frappe.listview_settings["Subcontracting Receipt"] = { get_indicator: function (doc) { const status_colors = { - Draft: "grey", + Draft: "red", Return: "gray", "Return Issued": "grey", Completed: "green", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index 8ff5c8f27b0f..e0fa7923ef99 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -1075,18 +1075,42 @@ def test_subcontracting_receipt_cancel_with_batch(self): @change_settings("Buying Settings", {"auto_create_purchase_receipt": 1}) def test_auto_create_purchase_receipt(self): + from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order + fg_item = "Subcontracted Item SA1" service_items = [ { "warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Service Item 1", - "qty": 5, + "qty": 10, "rate": 100, "fg_item": fg_item, "fg_item_qty": 5, }, ] - sco = get_subcontracting_order(service_items=service_items) + + po = create_purchase_order( + rm_items=service_items, + is_subcontracted=1, + supplier_warehouse="_Test Warehouse 1 - _TC", + do_not_submit=True, + ) + po.append( + "taxes", + { + "account_head": "_Test Account Excise Duty - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "Excise Duty", + "doctype": "Purchase Taxes and Charges", + "rate": 10, + }, + ) + po.save() + po.submit() + + sco = get_subcontracting_order(po_name=po.name) + rm_items = get_rm_items(sco.supplied_items) itemwise_details = make_stock_in_entry(rm_items=rm_items) make_stock_transfer_entry( @@ -1094,11 +1118,24 @@ def test_auto_create_purchase_receipt(self): rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details), ) + scr = make_subcontracting_receipt(sco.name) + scr.items[0].qty = 3 scr.save() scr.submit() - self.assertTrue(frappe.db.get_value("Purchase Receipt", {"subcontracting_receipt": scr.name})) + pr_details = frappe.get_all( + "Purchase Receipt", + filters={"subcontracting_receipt": scr.name}, + fields=["name", "total_taxes_and_charges"], + ) + + self.assertTrue(pr_details) + + pr_qty = frappe.db.get_value("Purchase Receipt Item", {"parent": pr_details[0]["name"]}, "qty") + self.assertEqual(pr_qty, 6) + + self.assertEqual(pr_details[0]["total_taxes_and_charges"], 60) def test_use_serial_batch_fields_for_subcontracting_receipt(self): fg_item = make_item( @@ -1361,6 +1398,66 @@ def test_subcontracting_receipt_for_batch_materials_without_use_serial_batch_fie frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1) + def test_change_batch_for_raw_materials(self): + set_backflush_based_on("BOM") + + fg_item = make_item(properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name + rm_item1 = make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BNGS-.####", + } + ).name + + bom = make_bom(item=fg_item, raw_materials=[rm_item1]) + second_batch_no = None + for row in bom.items: + se = make_stock_entry( + item_code=row.item_code, + qty=1, + target="_Test Warehouse 1 - _TC", + rate=300, + ) + + se.reload() + se1 = make_stock_entry( + item_code=row.item_code, + qty=1, + target="_Test Warehouse 1 - _TC", + rate=300, + ) + + se1.reload() + + second_batch_no = get_batch_from_bundle(se1.items[0].serial_and_batch_bundle) + + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 1, + "rate": 100, + "fg_item": fg_item, + "fg_item_qty": 1, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + scr = make_subcontracting_receipt(sco.name) + scr.save() + scr.reload() + + scr.supplied_items[0].batch_no = second_batch_no + scr.supplied_items[0].use_serial_batch_fields = 1 + scr.submit() + scr.reload() + + batch_no = get_batch_from_bundle(scr.supplied_items[0].serial_and_batch_bundle) + self.assertEqual(batch_no, second_batch_no) + self.assertEqual(scr.items[0].rm_cost_per_qty, 300) + self.assertEqual(scr.items[0].service_cost_per_qty, 100) + def make_return_subcontracting_receipt(**args): args = frappe._dict(args) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json index 75e263e2c1cb..23a7e69669d3 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -207,7 +207,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Rate", - "options": "currency", + "options": "Company:company:default_currency", "print_width": "100px", "read_only": 1, "width": "100px" @@ -217,7 +217,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Amount", - "options": "currency", + "options": "Company:company:default_currency", "read_only": 1 }, { @@ -231,6 +231,7 @@ "fieldtype": "Currency", "label": "Raw Material Cost Per Qty", "no_copy": 1, + "options": "Company:company:default_currency", "read_only": 1 }, { @@ -239,6 +240,7 @@ "fieldname": "service_cost_per_qty", "fieldtype": "Currency", "label": "Service Cost Per Qty", + "options": "Company:company:default_currency", "read_only": 1, "reqd": 1 }, @@ -248,6 +250,7 @@ "fieldname": "additional_cost_per_qty", "fieldtype": "Currency", "label": "Additional Cost Per Qty", + "options": "Company:company:default_currency", "read_only": 1 }, { @@ -582,7 +585,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2024-03-29 15:42:43.425544", + "modified": "2024-12-06 15:23:58.680169", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Item", diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py index 1a4ce5b977a2..69f7ae73e7a5 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py @@ -28,6 +28,7 @@ class SubcontractingReceiptItem(Document): is_scrap_item: DF.Check item_code: DF.Link item_name: DF.Data | None + job_card: DF.Link | None manufacturer: DF.Link | None manufacturer_part_no: DF.Data | None page_break: DF.Check diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index b140fdab51f4..011a5bc371fe 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -144,7 +144,7 @@ def validate_doc(self): ): frappe.throw( _("{0} is not enabled in {1}").format( - frappe.bold("Track Service Level Agreement"), + frappe.bold(_("Track Service Level Agreement")), get_link_to_form("Support Settings", "Support Settings"), ) ) diff --git a/erpnext/templates/includes/issue_row.html b/erpnext/templates/includes/issue_row.html index a04f558509f6..b55712ab1894 100644 --- a/erpnext/templates/includes/issue_row.html +++ b/erpnext/templates/includes/issue_row.html @@ -18,7 +18,7 @@ {% if doc.status == "Open" %} {{ doc.priority }} {% else %} - {{ doc.status }} + {{ _(doc.status) }} {%- endif -%} diff --git a/erpnext/templates/includes/projects/project_row.html b/erpnext/templates/includes/projects/project_row.html index 686637a20146..ccb306afcdb0 100644 --- a/erpnext/templates/includes/projects/project_row.html +++ b/erpnext/templates/includes/projects/project_row.html @@ -20,7 +20,7 @@ {% else %} - {{ doc.status }} + {{ _(doc.status) }} {% endif %} {% if doc["_assign"] %} diff --git a/erpnext/templates/includes/transaction_row.html b/erpnext/templates/includes/transaction_row.html index a498ba0eefaf..061c9bd1796e 100644 --- a/erpnext/templates/includes/transaction_row.html +++ b/erpnext/templates/includes/transaction_row.html @@ -8,7 +8,7 @@
- {{doc.status}} + {{ _(doc.status) }}
diff --git a/erpnext/templates/pages/order.html b/erpnext/templates/pages/order.html index 6c59a9688dca..388feb9eba93 100644 --- a/erpnext/templates/pages/order.html +++ b/erpnext/templates/pages/order.html @@ -173,11 +173,11 @@

{{ doc.name }}

{{ d.item_code }}
- {{ html2text(d.description) | truncate(140) }} + {{ html2text(d.description or "") | truncate(140) }}
{{ _("Qty ") }}({{ d.get_formatted("qty") }})
-{% endmacro %} \ No newline at end of file +{% endmacro %} diff --git a/erpnext/templates/pages/order.py b/erpnext/templates/pages/order.py index 505399f4a5b9..dca5a0c7497d 100644 --- a/erpnext/templates/pages/order.py +++ b/erpnext/templates/pages/order.py @@ -4,6 +4,10 @@ import frappe from frappe import _ +from erpnext.accounts.doctype.payment_request.payment_request import ( + ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST, +) + def get_context(context): context.no_cache = 1 @@ -46,8 +50,10 @@ def get_context(context): ) context.available_loyalty_points = int(loyalty_program_details.get("loyalty_points")) - context.show_pay_button = "payments" in frappe.get_installed_apps() and frappe.db.get_single_value( - "Buying Settings", "show_pay_button" + context.show_pay_button = ( + "payments" in frappe.get_installed_apps() + and frappe.db.get_single_value("Buying Settings", "show_pay_button") + and context.doc.doctype in ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST ) context.show_make_pi_button = False if context.doc.get("supplier"): diff --git a/erpnext/templates/pages/projects.py b/erpnext/templates/pages/projects.py index e3e26fc82a6f..787c7c0069b1 100644 --- a/erpnext/templates/pages/projects.py +++ b/erpnext/templates/pages/projects.py @@ -51,7 +51,7 @@ def get_tasks(project, start=0, search=None, item_status=None): "parent_task", ], limit_start=start, - limit_page_length=10, + limit_page_length=100, ) task_nest = [] for task in tasks: diff --git a/erpnext/templates/pages/timelog_info.html b/erpnext/templates/pages/timelog_info.html index be13826444c9..9f9445661a07 100644 --- a/erpnext/templates/pages/timelog_info.html +++ b/erpnext/templates/pages/timelog_info.html @@ -38,7 +38,7 @@

{{ doc.name }}

- + diff --git a/erpnext/tests/test_perf.py b/erpnext/tests/test_perf.py index fc17b1dcbda3..db54ca973958 100644 --- a/erpnext/tests/test_perf.py +++ b/erpnext/tests/test_perf.py @@ -3,7 +3,7 @@ INDEXED_FIELDS = { "Bin": ["item_code"], - "GL Entry": ["voucher_type", "against_voucher_type"], + "GL Entry": ["voucher_no", "posting_date", "company", "party"], "Purchase Order Item": ["item_code"], "Stock Ledger Entry": ["warehouse"], } diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 5f6e6f761e03..9ef1d4bc63a3 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -4480,10 +4480,23 @@ Payment Reconciliation,Zahlungsabgleich, Receivable / Payable Account,Forderungen-/Verbindlichkeiten-Konto, Bank / Cash Account,Bank / Geldkonto, From Invoice Date,Ab Rechnungsdatum, -To Invoice Date,Um Datum Rechnung, -Minimum Invoice Amount,Mindestabrechnung, +To Invoice Date,Bis Rechnungsdatum, +Invoice Limit,Max. Anzahl Rechnungen, +From Payment Date,Ab Zahlungsdatum, +To Payment Date,Bis Zahlungsdatum, +Payment Limit,Max. Anzahl Zahlungen, +Minimum Invoice Amount,Minimaler Rechnungsbetrag, Maximum Invoice Amount,Maximaler Rechnungsbetrag, -System will fetch all the entries if limit value is zero.,"Das System ruft alle Einträge ab, wenn der Grenzwert Null ist.", +Minimum Payment Amount,Minimaler Zahlungsbetrag, +Maximum Payment Amount,Maximaler Zahlungsbetrag, +Filter on Invoice,Filter auf Rechnungsnr., +Filter on Payment,Filter auf Zahlungsnr., +"If you need to reconcile particular transactions against each other, then please select accordingly. If not, all the transactions will be allocated in FIFO order.","Wenn Sie bestimmte Transaktionen gegeneinander abgleichen müssen, wählen Sie diese bitte entsprechend aus. Andernfalls werden alle Transaktionen in FIFO-Reihenfolge zugewiesen.", +System will fetch all the entries if limit value is zero.,"Das System ruft alle Einträge ab, wenn die max. Anzahl Null ist.", +This filter will be applied to Journal Entry.,Dieser Filter wird auf Buchungssätze angewendet., +Unreconciled Entries,Nicht zugeordnete Buchungen, +Allocated Entries,Zugewiesene Buchungen, +Accounting Dimensions Filter,Filetr nach Buchhaltungsdimensionen, Get Unreconciled Entries,Nicht zugeordnete Buchungen aufrufen, Unreconciled Payment Details,Nicht abgeglichene Zahlungen, Invoice/Journal Entry Details,Einzelheiten zu Rechnungs-/Journalbuchungen, @@ -11816,3 +11829,285 @@ will be,wird sein, {} is a child company.,{} ist ein untergeordnetes Unternehmen., {} {} is already linked with another {},{} {} ist bereits mit einem anderen {} verknüpft, {} {} is already linked with {} {},{} {} ist bereits mit {} {} verknüpft, +A Transaction Deletion Document: {0} is triggered for {0},Eine Transaktion Löschungsdokument: {0} wird für {0} ausgelöst, +About Us Settings,"Einstellungen zu ""Über uns""", +Allow Internal Transfers at Arm's Length Price,Interne Übertragungen zum Fremdvergleichspreis zulassen, +Asset decapitalized after Asset Capitalization {0} was submitted,"Vermögenswert freigegeben, nachdem Anlagenaktivierung {0} gebucht wurde", +Auto Email Report,Auto Email-Bericht, +Auto close Opportunity Replied after the no. of days mentioned above,Automatische Schließungschaltung antwortete nach der oben genannten Anzahl von Tagen, +Avg Rate (Balance Stock),Durchschnittliche Rate (Lagerbestand), +Billing Interval in Subscription Plan must be Month to follow calendar months,"Abrechnungsintervall im Abonnementplan muss ""Monat"" sein, um Kalendermonate zu folgen", +Bulk Update,Massen-Update, +Can't disable batch wise valuation for active batches.,Sie können die chargenweise Bewertung für aktive Chargen nicht deaktivieren., +Can't disable batch wise valuation for items with FIFO valuation method.,Sie können die chargenweise Bewertung für Artikel mit FIFO-Bewertungsmethode nicht deaktivieren., +Cannot disable batch wise valuation for FIFO valuation method.,Sie können die chargenweise Bewertung für die FIFO-Bewertungsmethode nicht deaktivieren., +Cannot enqueue multi docs for one company. {0} is already queued/running for company: {1},Mehrere Dokumente für ein Unternehmen können nicht in die Warteschlange gestellt werden. {0} ist bereits in die Warteschlange gestellt/wird für das Unternehmen ausgeführt: {1}, +Contact Us Settings,Einstellungen zu „Kontaktieren Sie uns“, +Create Journal Entries,Buchungssätze erstellen, +Create a variant with the template image.,Eine Variante mit dem Vorlagenbild erstellen., +Create in Draft Status,In Entwurfsstatus erstellen, +Custom delimiters,Benutzerdefinierte Trennzeichen, +Deleted Documents,Gelöschte Dokumente, +Delimiter options,Trennzeichenoptionen, +Dependent Task {0} is not a Template Task,Abhängige Aufgabe {0} ist keine Vorlage einer Aufgabe, +Depreciation Entry Posting Status,Buchungsstatus des Abschreibungseintrags, +Depreciation Schedule View,Ansicht Abschreibungsplan, +Depreciation cannot be calculated for fully depreciated assets,Für vollständig abgeschriebene Vermögensgegenstände kann keine Abschreibung berechnet werden, +Do Not Use Batch-wise Valuation,Keine chargenweise Bewertung verwenden, +Domain Settings,Domäneneinstellungen, +Email Domain,E-Mail-Domain, +Enable Immutable Ledger,Unveränderliches Hauptbuch aktivieren, +Enable it if users want to consider rejected materials to dispatch.,"Aktivieren Sie diese Option, wenn Benutzer zurückgewiesenes Material für den Versand berücksichtigen möchten.", +Excess Materials Consumed,Überschüssige Materialien verbraucht, +Excess Transfer,Überschuss-Übertragung, +FIFO Queue vs Qty After Transaction Comparison,Vergleich zwischen FIFO-Warteschlange und Menge nach Transaktion, +"For the {0}, the quantity is required to make the return entry","Für die {0} ist die Menge erforderlich, um die Retoure zu erstellen", +"If enabled, the item rate won't adjust to the valuation rate during internal transfers, but accounting will still use the valuation rate.","Falls aktiviert, wird der Artikelkurs bei internen Transfers nicht an den Bewertungskurs angepasst, aber die Buchhaltung verwendet weiterhin den Wertansatz.", +"If enabled, the system will use the moving average valuation method to calculate the valuation rate for the batched items and will not consider the individual batch-wise incoming rate.","Falls aktiviert, verwendet das System die Bewertungsmethode des gleitenden Durchschnitts zur Berechnung des Wertansatzes für die chargenweisen Artikel und berücksichtigt nicht den individuellen chargenweisen Eingangskurs.", +Job Worker,Unterauftragnehmer, +Job Worker Address,Unterauftragnehmer Adresse, +Job Worker Address Details,Vorschau Adresse Unterauftragnehmer, +Job Worker Contact,Vertrag des Unterauftragnehmers, +Job Worker Delivery Note,Lieferschein des Unterauftragnehmers, +Job Worker Name,Name des Unterauftragnehmer, +Job Worker Warehouse,Lagerhaus des Unterauftragnehmers, +"Learn about Common Party","Erfahren Sie mehr über die Verknüpfung von Kunden und Lieferanten", +Notification,Benachrichtigung, +Notification Settings,Benachrichtigungseinstellungen, +Offsetting for Accounting Dimension,Verrechnung für Buchhaltungsdimension, +Only Include Allocated Payments,Nur zugeordnete Zahlungen einbeziehen, +Only one {0} entry can be created against the Work Order {1},Nur ein {0} Eintrag kann gegen den Arbeitsauftrag {1} erstellt werden, +Over Picking Allowance,Überkommissionierzugabe, +Over Transfer Allowance,Überschlusstransferzugabe, +Overbilling of {} ignored because you have {} role.,"Überhöhte Abrechnung von {} wurde ignoriert, weil Sie die Rolle {} haben.", +Payment Reconciliation Job: {0} is running for this party. Can't reconcile now.,Job für Zahlungsabgleich: {0} läuft für diese Partei. Kann jetzt nicht abgleichen., +Payment Request created from Sales Order or Purchase Order will be in Draft status. When disabled document will be in unsaved state.,"Eine Zahlungsanforderung, die aus einem Auftrag oder einer Bestellung erstellt wurde, wird im Entwurfsstatus sein. Wenn deaktiviert, wird das Dokument in ungespeichertem Zustand sein.", +Payment Request took too long to respond. Please try requesting for payment again.,"Zahlungsaufforderung hat zu lange gedauert, um zu antworten. Bitte versuchen Sie die Zahlung erneut anzufragen.", +Payment Terms Status for Sales Order,Status für Zahlungsbedingungen für Aufträge, +Pipeline By,Pipeline von, +Please enable Use Old Serial / Batch Fields to make_bundle,"Bitte aktivieren Sie ""Alte Serien-/Batchfelder verwenden"" für make_bundle", +Print Style,Druckstil, +Reconcile All Serial Nos / Batches,Alle Seriennummern/Chargen abgleichen, +Reset Company Default Values,Standardwerte des Unternehmens zurücksetzen, +Reset Raw Materials Table,Tabelle Rohstoffe zurücksetzen, +Return Against Subcontracting Receipt,Retoure gegen Unterauftragsbeleg, +Return Components,Komponenten zurückgeben, +Returned Against,Zurückgegeben gegen, +Returned exchange rate is neither integer not float.,Der zurückgegebene Wechselkurs ist weder Integer noch Float., +Round Off Tax Amount,Steuerbetrag abrunden, +Rounding Loss Allowance,Rundungsverlusttoleranz, +Rounding Loss Allowance should be between 0 and 1,Rundungsverlusttoleranz muss zwischen 0 und 1 sein, +Row #{0}: Rejected Warehouse is mandatory for the rejected Item {1},Zeile #{0}: Ausschusslager ist für den abgelehnten Artikel {1} obligatorisch, +Row #{0}: You cannot use the inventory dimension '{1}' in Stock Reconciliation to modify the quantity or valuation rate. Stock reconciliation with inventory dimensions is intended solely for performing opening entries.,"Zeile #{0}: Sie können die Bestandsdimension '{1}' in der Bestandsabgleich nicht verwenden, um die Menge oder den Wertansatz zu ändern. Die Bestandsabgleich mit Bestandsdimensionen ist ausschließlich für die Durchführung von Eröffnungsbuchungen vorgesehen.", +Row {0}: Packed Qty must be equal to {1} Qty.,Zeile {0}: Verpackte Menge muss gleich der {1} Menge sein., +Row {0}: Total Number of Depreciations cannot be less than or equal to Opening Number of Booked Depreciations,Zeile {0}: Die Gesamtzahl der Abschreibungen kann nicht kleiner oder gleich der Anzahl der gebuchten Abschreibungen zu Beginn sein, +SCO Supplied Item,Artikel beigestellt für Unterauftrag, +SLA Fulfilled On Status,SLA erfüllt am Status, +SLA will be applied if {1} is set as {2}{3},"SLA wird angewendet, wenn {1} als {2}{3} eingestellt ist", +SMS Settings,SMS-Einstellungen, +SO Total Qty,Kd.-Auftr.-Gesamtmenge, +"Sales Order {0} already exists against Customer's Purchase Order {1}. To allow multiple Sales Orders, Enable {2} in {3}","Auftrag {0} existiert bereits für die Kundenbestellung {1}. Um mehrere Verkaufsaufträge zuzulassen, aktivieren Sie {2} in {3}", +"Scorecard variables can be used, as well as: +{total_score} (the total score from that period), +{period_number} (the number of periods to present day) +","Variablen der Bewertung können verwendet werden, sowie: +{total_score} (die Gesamtpunktzahl aus diesem Zeitraum), +{period_number} (die Anzahl der Zeiträume bis zum heutigen Tag) +", +Select Accounting Dimension.,Buchhaltungsdimension auswählen, +Select Corrective Operation,Nacharbeit auswählen, +Select Date of Birth. This will validate Employees age and prevent hiring of under-age staff.,Wählen Sie Geburtsdatum. Damit wird das Alter der Mitarbeiter überprüft und die Einstellung von minderjährigen Mitarbeitern verhindert., +"Select Date of joining. It will have impact on the first salary calculation, Leave allocation on pro-rata bases.",Wählen Sie Eintrittsdatum. Es wirkt sich auf die erste Gehaltsberechnung und die Zuteilung von Abwesenheiten auf Pro-rata-Basis aus., +Select Dimension,Dimension auswählen, +Select Items for Quality Inspection,Artikel für die Qualitätsprüfung auswählen, +Select Job Worker Address,Unterauftragnehmer Adresse auswählen, +Service Expenses,Wartungsaufwand, +Service Level Agreement for {0} {1} already exists.,Service Level Agreement für {0} {1} existiert bereits., +System Settings,Systemverwaltung, +Website Script,Webseiten-Skript, +Website Theme,Webseiten-Thema, +Workflow Action,Workflow-Aktion, +Workflow State,Workflow-Status, +{0} is not running. Cannot trigger events for this Document,{0} läuft nicht. Ereignisse für dieses Dokument können nicht ausgelöst werden, +"""SN-01::10"" for ""SN-01"" to ""SN-10""","""SN-01::10"" für ""SN-01"" bis ""SN-10""", +"Masters & Reports","Stammdaten & Berichte", +"Quick Access","Schnellzugriff", +"Reports & Masters","Berichte & Stammdaten", +"Reports & Masters","Berichte & Stammdaten", +"Settings","Einstellungen", +"Shortcuts","Verknüpfungen", +"Your Shortcuts + + + + + + ","Ihre Verknüpfungen + + + + + + ", +"Your Shortcuts","Ihre Verknüpfungen", +Grand Total: {0},Gesamtsumme:{0}, +Outstanding Amount: {0},Ausstehender Betrag: {0}, +Against Customer Order {0},Gegen Kundenauftrag {0}, +Against Supplier Invoice {0},Gegen Lieferantenrechnung {0}, +Ageing Range,Alterungsbereich, +Allocate Payment Request,Zahlungsanfrage zuweisen, +Amount in party's bank account currency,Betrag in der Währung des Bankkontos des Beteiligten, +Amount in transaction currency,Betrag in Transaktionswährung, +BIN Qty,BIN Menge, +BOM and Production,Stückliste und Produktion, +Balance Stock Value,Bestandswert, +Base Cost Per Unit,Grundkosten pro Einheit, +Base Rate,Basispreis, +Base Tax Withholding Net Total,Basis-Steuereinbehalt-Nettosumme, +Base Total,Basis-Summe, +Base Total Billable Amount,Basis Gesamter abrechenbarer Betrag, +Batch Expiry Date,Ablaufdatum der Charge, +Bill for Rejected Quantity in Purchase Invoice,Rechnung für abgelehnte Menge in der Eingangsrechnung, +Calculate daily depreciation using total days in depreciation period,Tägliche Abschreibung anhand der Gesamttage im Abschreibungszeitraum berechnen, +Call Schedule Row {0}: To time slot should always be ahead of From time slot.,Anrufplanzeile {0}: Das Zeitfenster Bis sollte immer vor dem Zeitfenster Von liegen., +Cannot find a default warehouse for item {0}. Please set one in the Item Master or in Stock Settings.,Es wurde kein Standardlager für den Artikel {0} gefunden. Bitte legen Sie eines im Artikelstamm oder in den Lagereinstellungen fest., +Cannot {0} from {2} without any negative outstanding invoice,Kann nicht {0} von {2} ohne negative ausstehende Rechnung, +Cheques and Deposits Incorrectly cleared,Falsch verrechnete Schecks und Einzahlungen, +Columns are not according to template. Please compare the uploaded file with standard template,Die Spalten stimmen nicht mit der Vorlage überein. Bitte vergleichen Sie die hochgeladene Datei mit der Standardvorlage, +Company is mandatory,Unternehmen ist obligatorisch, +Completion Date can not be before Failure Date. Please adjust the dates accordingly.,Das Fertigstellungsdatum kann nicht vor dem Ausfalldatum liegen. Bitte passen Sie die Daten entsprechend an., +Consumed Stock Items or Consumed Asset Items are mandatory for creating new composite asset,Verbrauchte Lagerartikel oder verbrauchte Vermögensgegenstand-Artikel sind für die Erstellung obligatorisch, +Convert to Item Based Reposting,Umstellung auf artikelbasiertes Umbuchen, +Create Workstation,Arbeitsplatz erstellen, +Creating Journal Entries...,Journaleinträge erstellen..., +Creating Purchase Invoices ...,Eingangsrechnungen erstellen ..., +Creating Sales Invoices ...,Ausgangsrechnungen erstellen ..., +Currency Exchange Settings Result,Währungsumtauscheinstellungen Ergebnis, +Deal Owner,Besitzer des Deals, +Decapitalized,Dekapitalisiert, +Dependant SLE Voucher Detail No,Unterhaltsberechtigter SLE Beleg Detail Nr., +Disassemble,Demontage, +Documents: {0} have deferred revenue/expense enabled for them. Cannot repost.,Dokumente: {0} hat vertagte Einnahmen/Ausgaben aktiviert. Kann nicht erneut posten., +Don't Reserve Sales Order Qty on Sales Return,Menge des Auftrags nicht bei der Rücksendung reservieren, +Enter Manually,Manuell eingeben, +Failed to post depreciation entries,Abschreibungsbuchungen fehlgeschlagen, +Filters missing,Filter fehlen, +"For Return Invoices with Stock effect, '0' qty Items are not allowed. Following rows are affected: {0}",Bei Retourenrechnungen mit Lagereffekt sind Artikel mit einer Menge von '0' nicht zulässig. Folgende Zeilen sind betroffen: {0}, +"For the item {0}, the quantity should be {1} according to the BOM {2}.",Für den Artikel {0} sollte die Menge gemäß Stückliste {2} {1} betragen., +"For the {0}, no stock is available for the return in the warehouse {1}.",Für {0} ist im Lager {1} kein Bestand für die Retoure verfügbar., +Force-Fetch Subscription Updates,Abonnement-Updates erzwingen, +From Date is mandatory,Von-Datum ist obligatorisch, +From Prospect,Von Interessenten, +Gross Purchase Amount Too Low: {0} cannot be depreciated over {1} cycles with a frequency of {2} depreciations.,Bruttokaufbetrag zu niedrig: {0} kann nicht über {1} Zyklen mit einer Häufigkeit von {2} Abschreibungen abgeschrieben werden., +If enabled then system won't apply the pricing rule on the delivery note which will be create from the pick list,"Falls aktiviert, wird das System die Preisregel nicht auf den Lieferschein anwenden, der aus der Pickliste erstellt wird", +Impairment,Wertminderung, +Include Closed Orders,Geschlossene Aufträge/Bestellungen einbeziehen, +Invalid Allocated Amount,Ungültiger zugewiesener Betrag, +Invalid Amount,Ungültiger Betrag, +Is Standard,Ist Standard, +Item {0} does not exist.,Artikel {0} existiert nicht., +Items {0} do not exist in the Item master.,Artikel {0} sind nicht im Artikelstamm vorhanden., +Journal entries have been created,Journaleinträge wurden erstellt, +Net total calculation precision loss,Präzisionsverlust bei Berechnung der Nettosumme, +Only Deduct Tax On Excess Amount ,Nur den überschüssigen Betrag versteuern , +Payment Ledger Entry,Zahlungsbucheintrag, +Payment Requests cannot be created against: {0},Zahlungsanforderungen können nicht erstellt werden für: {0}, +Period Closing Entry For Current Period,Periodenabschlussbuchung für aktuelle Periode, +Provisional Account,Vorläufiges Konto, +Rate of Stock UOM,Einzelpreis der Lager-ME, +Raw Materials Consumption ,Rohstoffverbrauch , +Recalculating Purchase Cost against this Project...,Neuberechnung der Anschaffungskosten für dieses Projekt..., +Reference DocType,Referenz DocType, +Round Tax Amount Row-wise,Steuerbetrag zeilenweise runden, +Rounding gain/loss Entry for Stock Transfer,Rundungsgewinn/-verlustbuchung für Umlagerung, +Row #{0}: Only {1} available to reserve for the Item {2},Zeile #{0}: Nur {1} zur Reservierung für den Artikel {2} verfügbar, +Row #{}: The original Invoice {} of return invoice {} is not consolidated.,Zeile #{}: Die ursprüngliche Rechnung {} der Rechnungskorrektur {} ist nicht konsolidiert., +Row {0}: Item {1} must be a subcontracted item.,Zeile {0}: Artikel {1} muss ein an Dritte vergebener Artikel sein., +Row {0}: Please provide a valid Delivery Note Item or Packed Item reference.,Zeile {0}: Bitte geben Sie einen gültigen Lieferschein Artikel oder verpackten Artikel an., +Row {0}: Target Warehouse is mandatory for internal transfers,Zeile {0}: Ziellager ist für interne Transfers obligatorisch, +Row({0}): Outstanding Amount cannot be greater than actual Outstanding Amount {1} in {2},Zeile({0}): Ausstehender Betrag kann nicht größer sein als der tatsächliche ausstehende Betrag {1} in {2}, +Rows with Same Account heads will be merged on Ledger,Zeilen mit denselben Konten werden im Hauptbuch zusammengefasst, +Select Warehouses to get Stock for Materials Planning,"Wählen Sie Lager aus, um Bestände für die Materialplanung zu erhalten", +Select an invoice to load summary data,"Wählen Sie eine Rechnung aus, um die Zusammenfassung zu laden", +Serial / Batch Bundle,Serien- / Chargenbündel, +Serial / Batch Bundle Missing,Serien- / Chargenbündel fehlt, +Serial No Range,Seriennummernbereich, +Serial and Batch Details,Serien- und Chargendetails, +Sets 'Accepted Warehouse' in each row of the Items table.,Legt in jeder Zeile der Artikeltabelle das Annahmelager fest., +Sets 'Rejected Warehouse' in each row of the Items table.,Legt in jeder Zeile der Artikeltabelle das „Ausschusslager“ fest., +Shelf Life in Days,Haltbarkeitsdauer in Tagen, +Show Disabled Warehouses,Deaktivierte Lager anzeigen, +Show GL Balance,Hauptbuchsaldo anzeigen, +Show Pay Button in Purchase Order Portal,Schaltfläche „Bezahlen“ im Bestellportal anzeigen, +Show Taxes as Table in Print,Steuern als Tabelle im Druck anzeigen, +Show net values in opening and closing columns,Nettowerte in Eröffnungs- und Abschlussspalten anzeigen, +Show with upcoming revenue/expense,Mit kommenden Einnahmen/Ausgaben anzeigen, +Something went wrong please try again,"Etwas ist schief gelaufen, bitte versuchen Sie es erneut", +South Africa VAT Account,Südafrika Mehrwertsteuer-Konto, +South Africa VAT Settings,Südafrika Mehrwertsteuer-Einstellungen, +Start Date should be lower than End Date,Das Startdatum muss vor dem Enddatum liegen, +Start Deletion,Löschen starten, +Start Time can't be greater than or equal to End Time for {0}.,Die Startzeit kann nicht größer oder gleich der Endzeit für {0} sein., +Started a background job to create {1} {0},Hintergrundjob zum Erstellen von {1} {0} gestartet, +Status set to rejected as there are one or more rejected readings.,"Der Status wurde auf abgelehnt gesetzt, da es einen oder mehrere abgelehnte Messwerte gibt.", +Stock Consumed During Repair,Während der Reparatur verbrauchter Bestand, +Stock Consumption Details,Details zum Lagerverbrauch, +Stock Planning,Bestandsplanung, +Stock Reservation,Bestandsreservierung, +Stock Reservation Entries Cancelled,Bestandsreservierungen storniert, +Stock Reservation Entries Created,Bestandsreservierungen erstellt, +Stock Reservation Entry,Bestandsreservierungseintrag, +Stock Reservation Entry cannot be updated as it has been delivered.,"Der Bestandsreservierungseintrag kann nicht aktualisiert werden, da er bereits geliefert wurde.", +"Stock Reservation Entry created against a Pick List cannot be updated. If you need to make changes, we recommend canceling the existing entry and creating a new one.","Ein anhand einer Kommissionierliste erstellter Bestandsreservierungseintrag kann nicht aktualisiert werden. Wenn Sie Änderungen vornehmen müssen, empfehlen wir, den vorhandenen Eintrag zu stornieren und einen neuen zu erstellen.", +Stock Reservation can only be created against {0}.,Bestandsreservierungen können nur gegen {0} erstellt werden., +Stock Reserved Qty (in Stock UOM),Reservierter Bestand (in Lager-ME), +Stock Unreservation,Aufhebung der Bestandsreservierung, +Stock/Accounts can not be frozen as processing of backdated entries is going on. Please try again later.,"Lagerbestände/Konten können nicht eingefroren werden, da die Verarbeitung rückwirkender Einträge noch läuft. Bitte versuchen Sie es später erneut.", +Supplied Item,Gelieferter Artikel, +Supplies subject to the reverse charge provision,"Lieferungen, die der Reverse-Charge-Regelung unterliegen", +Task {0} depends on Task {1}. Please add Task {1} to the Tasks list.,Aufgabe {0} hängt von Aufgabe {1} ab. Bitte fügen Sie Aufgabe {1} zur Aufgabenliste hinzu., +Tax Amount will be rounded on a row(items) level,Der Steuerbetrag wird auf (Artikel-)Zeilenebene gerundet, +Tax Refunds provided to Tourists under the Tax Refunds for Tourists Scheme,Steuererstattungen für Touristen im Rahmen der Steuererstattungsregelung für Touristen, +"Tax detail table fetched from item master as a string and stored in this field. +Used for Taxes and Charges","Steuerdetailtabelle, die aus dem Artikelstamm als Zeichenfolge abgerufen und in diesem Feld gespeichert wird. +Wird für Steuern und Gebühren verwendet", +"The Payment Request {0} is already paid, cannot process payment twice","Die Auszahlungsanforderung {0} ist bereits bezahlt, die Zahlung kann nicht zweimal verarbeitet werden", +The Serial No at Row #{0}: {1} is not available in warehouse {2}.,Die Seriennummer in Zeile #{0}: {1} ist im Lager {2} nicht verfügbar., +The Work Order is mandatory for Disassembly Order,Der Arbeitsauftrag ist obligatorisch für den Demontageauftrag, +The allocated amount is greater than the outstanding amount of Payment Request {0},Der zugewiesene Betrag ist größer als der ausstehende Betrag der Zahlungsanforderung {0}, +The field {0} in row {1} is not set,Das Feld {0} in der Zeile {1} ist nicht gesetzt, +The following invalid Pricing Rules are deleted:,Die folgenden ungültigen Preisregeln werden gelöscht:, +The original invoice should be consolidated before or along with the return invoice.,Die Originalrechnung sollte vor oder zusammen mit der Erstattungsrechnung konsolidiert werden., +"The sync has started in the background, please check the {0} list for new records.",Die Synchronisierung wurde im Hintergrund gestartet. Bitte überprüfen Sie die Liste {0} auf neue Datensätze., +"The users with this Role are allowed to create/modify a stock transaction, even though the transaction is frozen.","Die Benutzer mit dieser Rolle dürfen eine Lagerbewegungen erstellen/ändern, auch wenn die Transaktion eingefroren ist.", +There are no active Fiscal Years for which Demo Data can be generated.,"Es gibt keine aktiven Geschäftsjahre, für die Demodaten erstellt werden können.", +There were issues unlinking payment entry {0}.,Es gab Probleme bei der Aufhebung der Verknüpfung der Zahlung {0}., +"This is enabled by default. If you want to plan materials for sub-assemblies of the Item you're manufacturing leave this enabled. If you plan and manufacture the sub-assemblies separately, you can disable this checkbox.","Diese Option ist standardmäßig aktiviert. Wenn Sie Materialien für Unterbaugruppen des Artikels, den Sie herstellen, planen möchten, lassen Sie diese Option aktiviert. Wenn Sie die Unterbaugruppen separat planen und herstellen, können Sie dieses Kontrollkästchen deaktivieren.", +To Date is mandatory,Bis Datum ist obligatorisch, +To Delivery Date,Bis Liefertermin, +To Due Date,Bis Fälligkeitsdatum, +To Reference Date,Bis Stichtag, +To cancel a {} you need to cancel the POS Closing Entry {}.,"Um einen {} zu stornieren, müssen Sie die POS-Abschlussbuchung {} stornieren.", +Total Incoming Value (Receipt),Gesamter eingehender Wert (Empfang), +Total Number of Booked Depreciations ,Gesamtzahl der gebuchten Abschreibungen , +Total Operation Time,Gesamtbetriebszeit, +Total Other Charges,Sonstige Kosten insgesamt, +Total Purchase Amount,Gesamtkaufbetrag, +Total Purchase Cost has been updated,Die Gesamteinkaufskosten wurden aktualisiert, +UnReconcile,Zuordnung aufheben, +Unrealized Profit / Loss account for intra-company transfers,Konto für nicht realisierte Gewinne/Verluste aus konzerninternen Transfers, +Unrealized Profit/Loss account for intra-company transfers,Konto für nicht realisierte Gewinne/Verluste aus konzerninternen Transfers, +Validate Components Quantities Per BOM,Anzahl der Komponenten pro Stückliste überprüfen, +Validate Pricing Rule,Preisregel validieren, +Validate Stock on Save,Lagerbestand beim Speichern validieren, +Warning on Negative Stock,Warnung vor negativem Bestand, +You cannot create a {0} within the closed Accounting Period {1},Sie können innerhalb der abgeschlossenen Abrechnungsperiode {1} kein(e) {0} erstellen, +dated {0},von {0}, +must be between 0 and 100,muss zwischen 0 und 100 liegen, +or its descendants,oder seine Nachkommen, +subscription is already cancelled.,abonnement ist bereits storniert., +{0} Account not found against Customer {1}.,{0} Konto für Kunde {1} nicht gefunden., +{0} Transaction(s) Reconciled,{0} Transaktion(en) Abgestimmt, +{0} cannot be zero,{0} kann nicht Null sein, +{0} is already running for {1},{0} läuft bereits für {1}, +{0} units of Item {1} is not available in any of the warehouses.,{0} Einheiten des Artikels {1} sind in keinem der Lager verfügbar., diff --git a/erpnext/translations/es.csv b/erpnext/translations/es.csv index 1b0468d20772..dcb95fe54668 100644 --- a/erpnext/translations/es.csv +++ b/erpnext/translations/es.csv @@ -8743,3 +8743,2528 @@ WhatsApp,WhatsApp, Make a call,Haz una llamada, Approve,Aprobar, Reject,Rechazar, + Address, Dirección, + Amount, Importe, + Is Child Table, Es una tabla secundaria, + Name, Nombre, + Rate, Precio, + Summary, Resumen, +"""SN-01::10"" for ""SN-01"" to ""SN-10""","""SN-01::10"" para ""SN-01"" a ""SN-10""", +# In Stock,# En stock, +# Req'd Items,# Artículos Requeridos, +% Finished Item Quantity,% Cantidad de Artículos Terminados, +% Occupied,% Ocupado, +% Picked,% Seleccionado, +% Process Loss,% Pérdida por Proceso, +% Returned,% Devuelto, +'Account' in the Accounting section of Customer {0},'Cuenta' en la sección Contabilidad de Cliente {0}, +'Allow Multiple Sales Orders Against a Customer's Purchase Order','Permitir múltiples órdenes de venta contra la orden de compra de un cliente', +'Default {0} Account' in Company {1},'Cuenta {0} Predeterminada' en la Compañía {1}, +'To Package No.' cannot be less than 'From Package No.','Al paquete n.°' no puede ser menor que 'Desde el paquete n.°', +'{0}' account is already used by {1}. Use another account.,La cuenta de '{0}' ya está siendo utilizada por {1}. Utilice otra cuenta., +'{0}' should be in company currency {1}.,'{0}' debe estar en la moneda de la empresa {1}., +(A) Qty After Transaction,(A) Cant. después de la transacción, +(B) Expected Qty After Transaction,(B) Cant. esperada después de la transacción, +(C) Total Qty in Queue,(C) Cant. total en cola, +(C) Total qty in queue,(C) Cant. total en cola, +(D) Balance Stock Value,(D) Valor del balance de las existencias, +(E) Balance Stock Value in Queue,(E) Valor del balance de las existencias en cola, +(F) Change in Stock Value,(F) Cambio en el Valor de Stock, +(G) Sum of Change in Stock Value,(G) Suma del Cambio en el Valor de Stock, +(H) Change in Stock Value (FIFO Queue),(H) Cambio en Valor de Stock (Cola FIFO), +(H) Valuation Rate,(H) Tasa de valoración, +(I) Valuation Rate,(I) Tasa de valoración, +(J) Valuation Rate as per FIFO,(J) Tasa de valoración según FIFO, +(K) Valuation = Value (D) ÷ Qty (A),(K) Valoración = Valor (D) ÷ Cant. (A), +", with the inventory {0}: {1}",", con el inventario {0}: {1}", +0-30 Days,0-30 días, +1000+,más de 1.000, +11-50,11 a 50, +3 Yearly,3 Anual, +30-60 Days,30-60 días, +60-90 Days,60-90 días, +90 Above,Superior a 90, +"
+

Note

+
    +
  • +You can use Jinja tags in Subject and Body fields for dynamic values. +
  • + All fields in this doctype are available under the doc object and all fields for the customer to whom the mail will go to is available under the customer object. +
+

Examples

+ +
    +
  • Subject:

    Statement Of Accounts for {{ customer.customer_name }}

  • +
  • Body:

    +
    Hello {{ customer.customer_name }},
    PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}.
  • +
+","
+

Nota

+
    +
  • +Puede usar las etiquetas Jinja en los campos Asunto y Cuerpo para valores dinámicos. +
  • + Todos los campos de este doctype están disponibles en el objeto doc y todos los campos del cliente al que se enviará el correo están disponibles en el objeto customer . +
+

Ejemplos

+ +
    +
  • Asunto:

    Estado de Cuentas para {{ customer.customer_name }}

  • +
  • Cuerpo:

    +
    Hola {{ customer.customer_name }},
    PFA tu Estado de Cuenta del {{ doc.from_date }} al {{ doc.to_date }}.
  • +
+", +"
Other Details
","
Otros detalles
", +"
No Matching Bank Transactions Found
","
No se han encontrado transacciones bancarias coincidentes
", +"
+

All dimensions in centimeter only

+
","
+

Todas las dimensiones solo en centímetros

+
", +"

About Product Bundle

+ +

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.

+

The package Item will have Is Stock Item as No and Is Sales Item as Yes.

+

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.

","

Acerca de la agrupación de productos

+ +

Agregue un grupo de Artículos en otro Artículo. Esto es útil si está agrupando ciertos Artículos en un paquete y mantiene existencias de los Artículos empaquetados y no del Artículo agregado.

+

El Artículo del paquete tendrá Es Artículo de Stock como No y Es Artículo de Venta como .

+

Ejemplo:

+

Si está vendiendo Portátiles y Mochilas por separado y tiene un precio especial si el cliente compra ambos, entonces el Portátil + Mochila será un nuevo Artículo de Paquete de Productos.

", +"

Currency Exchange Settings Help

+

There are 3 variables that could be used within the endpoint, result key and in values of the parameter.

+

Exchange rate between {from_currency} and {to_currency} on {transaction_date} is fetched by the API.

+

Example: If your endpoint is exchange.com/2021-08-01, then, you will have to input exchange.com/{transaction_date}

","

Ayuda para la configuración del cambio de divisas

+

Hay 3 variables que se pueden utilizar dentro del endpoint, clave de resultado y en valores del parámetro.

+

El tipo de cambio entre {from_currency} y {to_currency} en {transaction_date} es obtenido por la API.

+

Ejemplo: Si su endpoint es exchange.com/2021-08-01, entonces, tendrá que introducir exchange.com/{transaction_date}

", +"

Body Text and Closing Text Example

+ +
We have noticed that you have not yet paid invoice {{sales_invoice}} for {{frappe.db.get_value(""Currency"", currency, ""symbol"")}} {{outstanding_amount}}. This is a friendly reminder that the invoice was due on {{due_date}}. Please pay the amount due immediately to avoid any further dunning cost.
+ +

How to get fieldnames

+ +

The fieldnames you can use in your template are the fields in the document. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Sales Invoice)

+ +

Templating

+ +

Templates are compiled using the Jinja Templating Language. To learn more about Jinja, read this documentation.

","

Ejemplo de cuerpo de texto y texto de cierre

+ +
Hemos observado que aún no ha pagado la factura {{sales_invoice}} correspondiente a {{frappe.db.get_value(""Currency"", currency, ""symbol"")}} {{outstanding_amount}}. Este es un recordatorio amistoso de que la factura vencía el {{due_date}}. Le rogamos que abone inmediatamente el importe adeudado para evitar posibles gastos de reclamación.
+ +

Cómo obtener nombres de campo

+ +

Los nombres de campo que puede utilizar en su plantilla son los campos del documento. Puede averiguar los campos de cualquier documento a través de Configuración > Personalizar vista de formulario y seleccionando el tipo de documento (por ejemplo, Factura de venta)

+ +

Plantillas

+ +

Las plantillas se compilan utilizando el lenguaje de plantillas Jinja. Para saber más sobre Jinja, lea esta documentación.

", +"

Contract Template Example

+ +
Contract for Customer {{ party_name }}
+
+-Valid From : {{ start_date }} 
+-Valid To : {{ end_date }}
+
+ +

How to get fieldnames

+ +

The field names you can use in your Contract Template are the fields in the Contract for which you are creating the template. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Contract)

+ +

Templating

+ +

Templates are compiled using the Jinja Templating Language. To learn more about Jinja, read this documentation.

","

Ejemplo de plantilla de contrato

+ +
Contrato para cliente {{ party_name }}
+
+-Válido desde : {{ start_date }} 
+-Válido hasta : {{ end_date }}
+
+ +

Cómo obtener los nombres de campo

+ +

Los nombres de campo que puede utilizar en su Plantilla de Contrato son los campos del Contrato para el que está creando la plantilla. Puede averiguar los campos de cualquier documento a través de Configuración > Personalizar vista de formulario y seleccionando el tipo de documento (por ejemplo, Contrato)

+ +

Creación de plantillas

+ +

Las plantillas se compilan utilizando el lenguaje de plantillas Jinja. Para saber más sobre Jinja, lea esta documentación.

", +"

Standard Terms and Conditions Example

+ +
Delivery Terms for Order number {{ name }}
+
+-Order Date : {{ transaction_date }} 
+-Expected Delivery Date : {{ delivery_date }}
+
+ +

How to get fieldnames

+ +

The fieldnames you can use in your email template are the fields in the document from which you are sending the email. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Sales Invoice)

+ +

Templating

+ +

Templates are compiled using the Jinja Templating Language. To learn more about Jinja, read this documentation.

","

Ejemplo de condiciones generales

+ +
Condiciones de entrega para el número de pedido {{ name }}
+
+-Fecha de pedido : {{ transaction_date }} 
+-Fecha de entrega prevista : {{ delivery_date }}
+
+ +

Cómo obtener los nombres de campo

+ +

Los nombres de campo que puede utilizar en su plantilla de correo electrónico son los campos del documento desde el que está enviando el correo electrónico. Puede averiguar los campos de cualquier documento a través de Configuración > Personalizar vista de formulario y seleccionando el tipo de documento (por ejemplo, Factura de venta)

+ +

Plantillas

+ +

Las plantillas se compilan utilizando el lenguaje de plantillas Jinja. Para saber más sobre Jinja, lea esta documentación.

", +"
Or
","
O
", +"","", +"","", +"","", +"

In your Email Template, you can use the following special variables: +

+
    +
  • + {{ update_password_link }}: A link where your supplier can set a new password to log into your portal. +
  • +
  • + {{ portal_link }}: A link to this RFQ in your supplier portal. +
  • +
  • + {{ supplier_name }}: The company name of your supplier. +
  • +
  • + {{ contact.salutation }} {{ contact.last_name }}: The contact person of your supplier. +
  • + {{ user_fullname }}: Your full name. +
  • +
+

+

Apart from these, you can access all values in this RFQ, like {{ message_for_supplier }} or {{ terms }}.

","

En su plantilla de correo electrónico, puede utilizar las siguientes variables especiales: +

+
    +
  • + {{ update_password_link }}: Un enlace donde su proveedor puede establecer una nueva contraseña para acceder a su portal. +
  • +
  • + {{ portal_link }}: Un enlace a esta petición de oferta en su portal de proveedores. +
  • +
  • + {{ supplier_name }}: El nombre de la empresa de su proveedor. +
  • +
  • + {{ contact.salutation }} {{ contact.last_name }}: La persona de contacto de su proveedor. +
  • + {{ user_fullname }}: Su nombre completo. +
  • +
+

+

Aparte de éstos, puede acceder a todos los valores de esta petición de oferta, como {{ message_for_supplier }} o {{ terms }}.

", +"
Message Example
+ +<p> Thank You for being a part of {{ doc.company }}! We hope you are enjoying the service.</p> + +<p> Please find enclosed the E Bill statement. The outstanding amount is {{ doc.grand_total }}.</p> + +<p> We don't want you to be spending time running around in order to pay for your Bill.
After all, life is beautiful and the time you have in hand should be spent to enjoy it!
So here are our little ways to help you get more time for life! </p> + +<a href=""{{ payment_url }}""> click here to pay </a> + +
+","
Ejemplo de mensaje
+ +<p> ¡Gracias por formar parte de {{ doc.company }}! Esperamos que esté disfrutando del servicio.</p> + +<p> Le adjuntamos el extracto de la factura E. El importe pendiente es de {{ doc.grand_total }}.</p> + +<p> No queremos que pierda tiempo dando vueltas para pagar su Factura.
¡Después de todo, la vida es bella y el tiempo de que dispone debe emplearlo en disfrutarla!
¡Así que aquí tiene nuestras pequeñas maneras de ayudarle a tener más tiempo para la vida! </p> + +<a href=""{{ payment_url }}""> pulse aquí para pagar </a> + +
+", +"
Message Example
+ +<p>Dear {{ doc.contact_person }},</p> + +<p>Requesting payment for {{ doc.doctype }}, {{ doc.name }} for {{ doc.grand_total }}.</p> + +<a href=""{{ payment_url }}""> click here to pay </a> + +
+","
Ejemplo de mensaje
+ +<p>Estimado {{ doc.contact_person }},</p> + +<p>Solicitando pago por {{ doc.doctype }}, {{ doc.name }} por {{ doc.grand_total }}.</p> + +<a href=""{{ payment_url }}""> Haga clic aquí para pagar </a> + +
+", +"Masters & Reports","Datos Maestros & Informes", +"Quick Access","Acceso rápido", +"Reports & Masters","Informes y Datos Maestros", +"Reports & Masters","Informes & Datos Maestros", +"Settings","Configuración", +"Shortcuts","Accesos directos", +"Your Shortcuts + + + + + + ","Tus accesos directos + + + + + + ", +"Your Shortcuts","Tus accesos directos", +Grand Total: {0},Total general: {0}, +Outstanding Amount: {0},Importe pendiente: {0}, +"
{d.batch_no}{d.serial_no}{abs(d.qty)}
{d.batch_no}{d.serial_no}{abs(d.qty)}
{d.batch_no}{abs(d.qty)}
+ + + + + + + + + + + + + + + + + +
Child DocumentNon Child Document
+

To access parent document field use parent.fieldname and to access child table document field use doc.fieldname

+ +
+

To access document field use doc.fieldname

+
+

Example: parent.doctype == ""Stock Entry"" and doc.item_code == ""Test""

+ +
+

Example: doc.doctype == ""Stock Entry"" and doc.purpose == ""Manufacture""

+
+ + + + + + +"," + + + + + + + + + + + + + + + + + +
Documento secundarioDocumento no secundario
+

Para acceder al campo del documento principal, utilice parent.fieldname y para acceder al campo del documento de la tabla secundaria, utilice doc.fieldname

+ +
+

Para acceder al campo del documento, utilice doc.fieldname

+
+

Ejemplo: parent.doctype == ""Entrada de stock"" y doc.item_code == ""Prueba""

+ +
+

Ejemplo: doc.doctype == ""Entrada de stock"" y doc.purpose == ""Fabricación""

+
+ + + + + + +", +A - B,A-B, +A - C,A-C, +A Holiday List can be added to exclude counting these days for the Workstation.,Se puede añadir una lista de días festivos para excluir el cómputo de estos días para el puesto de trabajo., +A Packing Slip can only be created for Draft Delivery Note.,Solo se puede crear un albarán para un borrador de nota de entrega., +"A Price List is a collection of Item Prices either Selling, Buying, or both","Una lista de precios es una colección de Precios de Productos, ya sea de Venta, de Compra o ambos", +A Reconciliation Job {0} is running for the same filters. Cannot reconcile now,Se está ejecutando un trabajo de reconciliación {0} para los mismos filtros. No se puede reconciliar ahora., +A Transaction Deletion Document: {0} is triggered for {0},Un documento de borrado de transacciones: {0} se activa para {0}, +A customer must have primary contact email.,Un cliente debe tener un correo electrónico de contacto principal., +A driver must be set to submit.,Debe seleccionar un conductor antes de confirmar., +A template with tax category {0} already exists. Only one template is allowed with each tax category,Ya existe una plantilla con categoría de impuestos {0}. Sólo se permite una plantilla con cada categoría de impuestos, +API Details,Detalles de la API, +AWB Number,Número AWB, +Abbreviation: {0} must appear only once,Abreviación: {0} debe aparecer sólo una vez, +About Us Settings,Configuración de información de la compañía, +About {0} minute remaining,Quedan aproximadamente {0} minutos, +About {0} minutes remaining,Quedan aproximadamente {0} minutos, +About {0} seconds remaining,Quedan aproximadamente {0} segundos, +Acceptance Criteria Formula,Fórmula de Criterio de Aceptación, +Acceptance Criteria Value,Valor de los Criterios de Aceptación, +Accepted Qty in Stock UOM,Cantidad Aceptada en UdM de Stock, +Access Key,Clave de Acceso, +Access Key is required for Service Provider: {0},Se requiere clave de acceso para el proveedor de servicios: {0}, +Account Balance (From),Saldo de cuenta (Desde), +Account Balance (To),Saldo de cuenta (Para), +Account Closing Balance,Balance de Cierre de Cuenta, +Account Currency (From),Moneda de la Cuenta (De), +Account Currency (To),Divisa de la Cuenta (De), +Account Opening Balance,Saldo de Apertura de Cuenta, +Account not Found,Cuenta no encontrada, +Account {0} added multiple times,Cuenta {0} agregada varias veces, +Accounting Dimension Filter,Filtro de dimensión contable, +Accounting Dimensions Filter,Filtro de dimensiones contables, +Accounting Entry for {0},Entrada contable para {0}, +Accounts Closing,Cierre de Cuentas, +Accounts Missing Error,Error de Cuentas Faltantes, +Accounts Receivable/Payable,Cuentas por Cobrar/Pagar, +Accounts to Merge,Cuentas a fusionar, +Action If Quality Inspection Is Rejected,Acción si se rechaza la inspección de calidad, +Action If Same Rate is Not Maintained,Acción si no se mantiene la misma tasa, +Action if Same Rate is Not Maintained Throughout Sales Cycle,Si no se mantiene la misma tarifa durante todo el ciclo de ventas, +Active Status,Estado activo, +Actual Balance Qty,Cantidad de Saldo Actual, +Actual Expense,Gasto actual, +Actual Posting,Asiento Actual, +Actual Qty in Warehouse,Cantidad real en Almacén, +Actual Time,Tiempo actual, +Add Columns in Transaction Currency,Añadir Columnas en Moneda de Transacción, +Add Corrective Operation Cost in Finished Good Valuation,Añadir Costo de Operación Correctiva en la Valoración de Productos Terminados, +Add Discount,Agregar descuento, +Add Items in the Purpose Table,Añadir Elementos en la Tabla de Propósitos, +Add Lead to Prospect,Añadir cliente potencial a prospecto, +Add Local Holidays,Agregar días festivos locales, +Add Manually,Añadir manualmente, +Add Or Deduct,Añadir o deducir, +Add Serial / Batch Bundle,Añadir Nro. Serie/Lote, +Add Serial / Batch No,Añadir Nro Serie/Lote, +Add Serial / Batch No (Rejected Qty),Añadir Nro Serie/Lote (Cant Rechazada), +Add Stock,Añadir Inventario, +Add Sub Assembly,Añadir sub ensamblaje, +Add Template,Añadir plantilla, +Add a Note,Añadir Nota, +Add details,Añadir detalles, +Add to Prospect,Añadir a prospectos, +Added By,Añadido por, +Added On,Añadido el, +Added Supplier Role to User {0}.,Añadido el Rol de Proveedor al Usuario {0}., +Added {1} Role to User {0}.,Se agregó el Rol {1} al Usuario {0}., +Adding Lead to Prospect...,Agregando cliente potencial a prospecto..., +Additional,Adicional, +Additional Asset Cost,Costo Adicional del Activo, +Additional Cost Per Qty,Costo adicional por cantidad, +Additional Info,Información Adicional, +Address And Contacts,Dirección y Contactos, +Adjust Asset Value,Ajustar el valor del activo, +Adjustment Against,Ajuste contra, +Adjustment based on Purchase Invoice rate,Ajuste basado en la tarifa de la Factura de Compra, +Advance Account: {0} must be in either customer billing currency: {1} or Company default currency: {2},La cuenta de anticipo: {0} debe estar en la moneda de facturación del cliente: {1} o en la moneda predeterminada de la empresa: {2}, +Advance Payment,Pago adelantado, +Advance Tax,Impuesto anticipado, +Advance Taxes and Charges,Impuestos y Cargos anticipados, +Advance paid against {0} {1} cannot be greater than Grand Total {2},El anticipo pagado contra {0} {1} no puede ser mayor que el total general {2}., +Advance payments allocated against orders will only be fetched,Los pagos anticipados asignados a pedidos solo se recuperarán., +Affected Transactions,Transacciones Afectadas, +Against Customer Order {0},Contra pedido del cliente {0}, +Against Supplier Invoice {0},Contra factura del proveedor {0}, +Against Voucher No,Contra el Número de Comprobante, +Age ({0}),Edad ({0}), +Ageing Range,Rango de antigüedad, +Agent Busy Message,Mensaje de agente ocupado, +Agent Group,Grupo de agentes, +Agent Unavailable Message,Mensaje de agente no disponible, +Aggregate a group of Items into another Item. This is useful if you are maintaining the stock of the packed items and not the bundled item,Agregue un grupo de Artículos en otro Artículo. Esto es útil si está manteniendo el stock de los artículos empaquetados y no del artículo agrupado, +Algorithm,Algoritmo, +All Activities,Todas las Actividades, +All Activities HTML,Todas las actividades HTML, +All Items,Todos los Productos, +All Sales Transactions can be tagged against multiple Sales Persons so that you can set and monitor targets.,Todas las transacciones de ventas se pueden etiquetar contra varias personas de ventas para que pueda establecer y supervisar los objetivos., +All allocations have been successfully reconciled,Todas las asignaciones se han reconciliado con éxito., +All items have already been received,Todos los artículos ya han sido recibidos, +All items in this document already have a linked Quality Inspection.,Todos los artículos de este documento ya tienen una Inspección de Calidad vinculada., +All the Comments and Emails will be copied from one document to another newly created document(Lead -> Opportunity -> Quotation) throughout the CRM documents.,Todos los comentarios y correos electrónicos se copiarán de un documento a otro recién creado (Cliente potencial -> Oportunidad -> Oferta) en todos los documentos del CRM., +"All the required items (raw materials) will be fetched from BOM and populated in this table. Here you can also change the Source Warehouse for any item. And during the production, you can track transferred raw materials from this table.","Todos los artículos necesarios (materias primas) se obtendrán de la lista de materiales y se rellenarán en esta tabla. Aquí también puede cambiar el Almacén de Origen para cualquier artículo. Y durante la producción, puede hacer un seguimiento de las materias primas transferidas desde esta tabla.", +Allocate Payment Request,Asignar solicitud de pago, +Allocated Entries,Entradas Asignadas, +Allocated To:,Asignado a:, +Allocations,Asignaciones, +Allow,Permitir, +Allow Alternative Item must be checked on Item {},Permitir elemento alternativo debe estar marcado en Elemento {}, +Allow Continuous Material Consumption,Permitir el consumo continuo de material, +Allow Excess Material Transfer,Permitir la transferencia de material sobrante, +Allow Internal Transfers at Arm's Length Price,Permitir Transferencias Internas a Precio de Mercado, +Allow Item to be Added Multiple Times in a Transaction,Permitir que un artículo se añada varias veces en una transacción, +Allow Lead Duplication based on Emails,Permitir la duplicación de clientes potenciales basada en correos electrónicos, +Allow Negative rates for Items,Permitir tarifas negativas para Productos, +Allow Or Restrict Dimension,Permitir o Restringir dimensión, +Allow Partial Reservation,Permitir reserva parcial, +Allow Purchase,Permitir Compra, +Allow Sales,Permitir Ventas, +Allow Sales Order Creation For Expired Quotation,Permitir la creación de Órdenes de Venta para Cotizaciones vencidas, +Allow User to Edit Discount,Permitir al usuario editar el descuento, +Allow User to Edit Rate,Permitir al usuario editar la tarifa, +Allow Zero Rate,Permitir Tarifa Cero, +Allow material consumptions without immediately manufacturing finished goods against a Work Order,Permitir consumos de material sin fabricar inmediatamente productos acabados contra una Orden de Trabajo, +Allow multi-currency invoices against single party account ,Permitir facturas en múltiples monedas contra una cuenta de una sola parte., +Allow to Edit Stock UOM Qty for Purchase Documents,Permitir editar la cantidad de UdM de stock para documentos de compras, +Allow to Edit Stock UOM Qty for Sales Documents,Permitir editar la cantidad de UdM de stock para documentos de ventas, +Allow transferring raw materials even after the Required Quantity is fulfilled,Permitir la transferencia de materias primas incluso después de cumplir la cantidad requerida, +Allowed,Permitido, +Allowed Dimension,Dimensión permitida, +Allowed Doctypes,Doctypes permitidos, +Allowed Items,Productos Permitidos, +Allowed primary roles are 'Customer' and 'Supplier'. Please select one of these roles only.,"Los roles principales permitidos son 'Cliente' y 'Proveedor'. Por favor, seleccione sólo uno de estos roles.", +Allows to keep aside a specific quantity of inventory for a particular order.,Permite reservar una cantidad específica de existencias para un pedido concreto., +Already Picked,Ya ha sido seleccionado, +Alternative Items,Ítems Alternativos, +"Alternatively, you can download the template and fill your data in.",También puede descargar la plantilla y rellenar sus datos., +Amount (AED),Importe del impuesto (AED), +Amount Eligible for Commission,Importe elegible para la comisión, +Amount in Account Currency,Importe en Moneda de la Cuenta, +Amount in party's bank account currency,Importe en la divisa de la cuenta bancaria., +Amount in transaction currency,Importe en la divisa de la transacción, +An Item Group is a way to classify items based on types.,Un Grupo de Producto es una forma de clasificar Productos según sus tipos., +An error has been appeared while reposting item valuation via {0},Se ha producido un error al volver a publicar la valoración del artículo a través de {0}, +An error has occurred during {0}. Check {1} for more details,Se ha producido un error durante {0}. Consulte {1} para obtener más detalles.,Error Log +Annual Revenue,Ingresos Anuales, +"Another Cost Center Allocation record {0} applicable from {1}, hence this allocation will be applicable upto {2}","Otro registro de Asignación de Centro de Coste {0} aplicable desde {1}, por lo tanto esta asignación será aplicable hasta {2}", +"Any one of following filters required: warehouse, Item Code, Item Group","Se requiere cualquiera de los siguientes filtros: almacén, código de artículo, grupo de artículos", +Applicable Dimension,Dimensión aplicable, +Applicable On Account,Aplicable en Cuenta, +Applied on each reading.,Se aplica en cada lectura., +Applied putaway rules.,Reglas de almacenamiento aplicadas., +Apply Putaway Rule,Aplicar la regla de almacenamiento, +Apply Recursion Over (As Per Transaction UOM),Aplicar recursión sobre (según UdM de transacción), +Apply SLA for Resolution Time,Aplicar SLA para el tiempo de resolución, +Apply TDS,Aplicar a, +Apply Tax Withholding Amount ,Aplicar Monto de Retención de Impuestos , +Apply restriction on dimension values,Aplicar restricción a los valores de dimensión, +Apply to All Inventory Documents,Aplicar a todos los documentos de inventario, +Apply to Document,Aplicar al documento, +Appointment Created Successfully,Cita creada exitosamente, +Appointment Scheduling Disabled,Programación de citas deshabilitada, +Appointment Scheduling has been disabled for this site,La programación de citas ha sido desactivada para este sitio, +Appointment was created. But no lead was found. Please check the email to confirm,"Se creó la cita, pero no se encontró ningún cliente potencial. Por favor, revise el correo electrónico para confirmar.", +Approximately match the description/party name against parties,Comparar aproximadamente la descripción/nombre de la parte con las partes., +Are you sure you want to clear all demo data?,¿Está seguro de que desea borrar todos los datos de la demostración?, +Are you sure you want to delete this Item?,¿Está seguro de que desea borrar este elemento?, +Are you sure you want to restart this subscription?,¿Está seguro de que desea reiniciar esta suscripción?, +As on Date,A fecha, +"As there are existing submitted transactions against item {0}, you can not change the value of {1}.","Como ya existen transacciones validadas contra el artículo {0}, no puede cambiar el valor de {1}.", +"As there are negative stock, you can not enable {0}.","Como hay existencias negativas, no puede habilitar {0}.", +"As there are reserved stock, you cannot disable {0}.","Como hay stock reservado, no puedes desactivar {0}.", +"As there are sufficient Sub Assembly Items, Work Order is not required for Warehouse {0}.","Dado que hay suficientes artículos de sub ensamblaje, no se requiere una orden de trabajo para el almacén {0}.", +"As {0} is enabled, you can not enable {1}.","Como {0} está habilitado, no puedes habilitar {1}.", +Assembly Items,Artículos de montaje, +Asset Activity,Actividad de Activos, +Asset Capitalization,Capitalización de Activos, +Asset Capitalization Asset Item,Elemento de Capitalización de Activos, +Asset Capitalization Service Item,Elemento de servicio de capitalización de activos, +Asset Capitalization Stock Item,Elemento de stock de capitalización de activos, +Asset Depreciation Details,Detalles de depreciación de activos, +Asset Depreciation Schedule,Calendario de depreciación de activos, +Asset Depreciation Schedule for Asset {0} and Finance Book {1} is not using shift based depreciation,Calendario de depreciación de activos para el activo {0} y el libro de finanzas {1} no está utilizando la depreciación por turnos, +Asset Depreciation Schedule not found for Asset {0} and Finance Book {1},El Calendario de amortización de activos no encontrado para el activo {0} y el libro de finanzas {1}, +Asset Depreciation Schedule {0} for Asset {1} already exists.,El Calendario de amortización de activos {0} para el activo {1} ya existe., +Asset Depreciation Schedule {0} for Asset {1} and Finance Book {2} already exists.,El Calendario de Depreciación de Activos {0} para el Activo {1} y el Libro Financiero {2} ya existe., +"Asset Depreciation Schedules created:
{0}

Please check, edit if needed, and submit the Asset.","Programas de depreciación de activos creados:
{0}

Verifique, edite si es necesario y valide el activo.", +Asset ID,Id de Activo, +Asset Quantity,Cantidad de Activos, +Asset Repair Consumed Item,Artículo Consumido en Reparación de Activos, +Asset Settings,Configuración de Activos, +Asset Shift Allocation,Asignación de Turnos de Activos, +Asset Shift Factor,Factor de cambio de activos, +Asset Shift Factor {0} is set as default currently. Please change it first.,El factor de cambio de activos {0} está configurado como predeterminado actualmente. Cámbielo primero., +Asset cancelled,Activo cancelado, +Asset capitalized after Asset Capitalization {0} was submitted,El Activo capitalizado fue validado después de la Capitalización de Activos {0}, +Asset created,Activo creado, +Asset created after Asset Capitalization {0} was submitted,El Activo creado fue validado después del la Capitalización de Activos {0}, +Asset created after being split from Asset {0},Activo creado después de ser separado del Activo {0}, +Asset decapitalized after Asset Capitalization {0} was submitted,El Activo descapitalizado fue validado después de la Capitalización de Activos {0}, +Asset deleted,Activo eliminado, +Asset issued to Employee {0},Activo emitido al empleado {0}, +Asset out of order due to Asset Repair {0},Activo fuera de servicio debido a la reparación del activo {0}, +Asset received at Location {0} and issued to Employee {1},Activo recibido en el lugar {0} y entregado al empleado {1}, +Asset restored,Activo restituido, +Asset restored after Asset Capitalization {0} was cancelled,Activo restituido después de la Capitalización de Activos {0} fue cancelada, +Asset returned,Activo devuelto, +Asset scrapped,Activo desechado, +Asset sold,Activo vendido, +Asset submitted,Activo validado, +Asset transferred to Location {0},Activo transferido a la ubicación {0}, +Asset updated after being split into Asset {0},Activo actualizado tras ser dividido en Activo {0}, +Asset updated after cancellation of Asset Repair {0},Activo actualizado tras la anulación de la reparación de activos {0}, +Asset updated after completion of Asset Repair {0},Activo actualizado tras la finalización de la reparación del activo {0}, +Asset {0} cannot be received at a location and given to an employee in a single movement,El activo {0} no puede recibirse en un lugar y entregarse a un empleado en un solo movimiento, +Asset {0} does not belong to Item {1},Activo {0} no pertenece al Producto {1}, +Asset {0} does not exist,Activo {0} no existe, +Asset {0} has been created. Please set the depreciation details if any and submit it.,"Se ha creado el activo {0}. Por favor, establezca los detalles de depreciación si los hay y valídelo.", +Asset {0} has been updated. Please set the depreciation details if any and submit it.,"El activo {0} ha sido actualizado. Por favor, establezca los detalles de depreciación si los hay y valídelo.", +Asset's depreciation schedule updated after Asset Shift Allocation {0},Calendario de amortización del activo actualizado tras la asignación del cambio de activo {0}, +Asset's value adjusted after cancellation of Asset Value Adjustment {0},Valor del activo ajustado tras la cancelación del ajuste del valor del activo {0}, +Asset's value adjusted after submission of Asset Value Adjustment {0},Valor del activo ajustado tras la presentación del ajuste del valor del activo {0}, +Assign Job to Employee,Asignar trabajo a un empleado, +Assignment,Asignación, +Assignment Conditions,Condiciones de asignación, +At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} for the batch {4} in the warehouse {5}.,En la fila #{0}: La cantidad seleccionada {1} para el artículo {2} es mayor que el stock disponible {3} para el lote {4} en el almacén {5}., +At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} in the warehouse {4}.,En la fila #{0}: La cantidad seleccionada {1} para el artículo {2} es mayor que el stock disponible {3} en el almacén {4}., +At row {0}: Batch No is mandatory for Item {1},En la fila {0}: el Núm. de Lote es obligatorio para el Producto {1}, +At row {0}: Parent Row No cannot be set for item {1},En la fila {0}: No se puede establecer el nº de fila padre para el artículo {1}, +At row {0}: Qty is mandatory for the batch {1},En la fila {0}: La cant. es obligatoria para el lote {1}, +At row {0}: Serial No is mandatory for Item {1},En la fila {0}: el Núm. Serial es obligatorio para el Producto {1}, +At row {0}: Serial and Batch Bundle {1} has already created. Please remove the values from the serial no or batch no fields.,"En la fila {0}: El paquete de serie y lote {1} ya está creado. Por favor, elimine los valores de los campos nº de serie o nº de lote.", +At row {0}: set Parent Row No for item {1},En la fila {0}: establezca el nº de fila padre para el artículo {1}, +Attach CSV File,Adjuntar archivo CSV, +Attendance & Leaves,Asistencia y Salidas, +Attribute value: {0} must appear only once,Valor del atributo: {0} debe aparecer sólo una vez, +Auto Create Exchange Rate Revaluation,Creación automática de la revalorización del Tipo de Cambio, +Auto Create Purchase Receipt,Creación automática de Recibo de Compra, +Auto Create Serial and Batch Bundle For Outward,Crear Automáticamente Lote y Serie para Salida, +Auto Create Subcontracting Order,Crear orden de subcontratación automáticamente, +Auto Created Serial and Batch Bundle,Creación automática de series y lotes, +Auto Creation of Contact,Creación automática de Contacto, +Auto Email Report,Reporte de Correo Electrónico Automático, +Auto Insert Item Price If Missing,Insertar automáticamente el precio del artículo si falta, +Auto Name,Nombre Automático, +Auto Reconcile,Reconciliación Automática, +Auto Reconcile Payments,Reconciliación Automática de Pagos, +Auto Reconciliation,Reconciliación Automática, +Auto Reconciliation of Payments has been disabled. Enable it through {0},Reconciliación automática de pagos ha sido desactivada. Habilítelo a través de {0}, +Auto Reserve Serial and Batch Nos,Reserva automática de números de serie y de lote, +Auto Reserve Stock for Sales Order on Purchase,Reserva automática de stock para órdenes de venta en el momento de la compra, +Auto close Opportunity Replied after the no. of days mentioned above,Cierre automático Oportunidad Respondida después del número de días mencionado anteriormente, +Auto match and set the Party in Bank Transactions,Coincidencia automática y fijación de la entidad en las Transacciones Bancarias, +Auto write off precision loss while consolidation,Cancelación automática de la pérdida de precisión durante la consolidación, +Automatically Add Filtered Item To Cart,Añadir automáticamente el artículo filtrado a la cesta, +Automatically Fetch Payment Terms from Order,Obtenga automáticamente las condiciones de pago del pedido, +Automatically post balancing accounting entry,Registrar automáticamente el asiento contable de balance, +Available Batch Report,Informe de lotes disponibles, +Available Qty For Consumption,Cantidad disponible para consumo, +Available Qty at Company,Cant. disponible en Compañía, +Available Qty at Target Warehouse,Cantidad disponible en Almacén de destino, +Available Qty to Reserve,Cantidad disponible para reservar, +Average Completion,Promedio de completado, +Avg Rate,Tasa promedio, +Avg Rate (Balance Stock),Tasa media (Balance Stock), +BFS,BFS (Búsqueda en Amplitud), +BIN Qty,Cant. BIN, +BOM Created,LdM Creado, +BOM Creator,Creador LdM, +BOM Creator Item,Creador de elementos de lista de materiales, +BOM Info,Información de LdM, +BOM Level,Nivel de lista de materiales, +BOM Tree,Árbol LdM, +BOM UoM,LdM UdM, +BOM Update Batch,Lote de actualización de lista de materiales, +BOM Update Initiated,Actualización de lista de materiales iniciada, +BOM Update Log,Registro de actualización de lista de materiales, +BOM Update Tool Log with job status maintained,Registro de la herramienta de actualización de lista de materiales con el estado del trabajo mantenido, +BOM Updation already in progress. Please wait until {0} is complete.,La actualización de la lista de materiales ya está en curso. Espere hasta que se complete {0} ., +BOM Updation is queued and may take a few minutes. Check {0} for progress.,La actualización de la lista de materiales está en cola y puede tardar unos minutos. Verifique {0} para ver el progreso., +BOM and Production,Lista de materiales y producción, +BOM recursion: {1} cannot be parent or child of {0},Recursión de la lista de materiales: {1} no puede ser padre o hijo de {0}, +BOMs Updated,Listas de materiales actualizadas, +BOMs created successfully,Listas de materiales creadas con éxito, +BOMs creation failed,La creación de listas de materiales falló, +"BOMs creation has been enqueued, kindly check the status after some time","La creación de listas de materiales se ha puesto en cola, compruebe el estado después de algún tiempo", +Balance Qty (Stock),Cantidad (stock), +Balance Sheet Summary,Resumen del balance general, +Balance Stock Value,Valor de stock, +Bank Reconciliation Tool,Herramienta de Reconciliación Bancaria, +Bank Statement Import,Importación de extractos bancarios, +Bank Transaction {0} Matched,Transacción bancaria {0} Cotejada, +Bank Transaction {0} added as Journal Entry,Transacción bancaria {0} añadida como asiento, +Bank Transaction {0} added as Payment Entry,Transacción bancaria {0} añadida como asiento de pago, +Bank Transaction {0} is already fully reconciled,Transacción bancaria {0} ya está totalmente conciliada, +Bank Transaction {0} updated,Transacción bancaria {0} actualizada, +Bank/Cash Account,Cuenta Banco/Efectivo, +Bank/Cash Account {0} doesn't belong to company {1},La Cuenta Banco/Efectivo {0} no pertenece a la compañía {1}, +Base Amount,Importe base, +Base Cost Per Unit,Coste base por unidad, +Base Rate,Tarifa base, +Base Tax Withholding Net Total,Base Imponible Retención Neta Total, +Base Total,Total base, +Base Total Billable Amount,Base Importe total facturable, +Base Total Billed Amount,Base Importe total facturado, +Base Total Costing Amount,Base Importe total del cálculo de costes, +Based On Value,Basado en el Valor, +"Based on your HR Policy, select your leave allocation period's end date","Basándose en su política de RRHH, seleccione la fecha de finalización del período de asignación de vacaciones", +"Based on your HR Policy, select your leave allocation period's start date","Basándose en su política de RRHH, seleccione la fecha de inicio de su período de asignación de vacaciones", +Batch Expiry Date,Fecha de caducidad del lote, +Batch No is mandatory,El número de lote es obligatorio, +Batch No {0} does not exists,Lote núm. {0} no existe, +Batch No {0} is linked with Item {1} which has serial no. Please scan serial no instead.,"El lote nº {0} está vinculado con el artículo {1} que tiene nº de serie. Por favor, escanee el nº de serie en su lugar.", +Batch No.,Nº de Lote, +Batch Nos,Números de Lote, +Batch Nos are created successfully,Los Núm. de Lote se crearon correctamente, +Batch Not Available for Return,Lote no disponible para devolución, +Batch Qty,Cantidad de lote, +Batch and Serial No,Núm. de Lote y Serie, +Batch not created for item {} since it does not have a batch series.,"Lote no creado para el artículo {}, ya que no tiene serie de lote.", +Batch {0} and Warehouse,Lote {0} y almacén, +Batch {0} is not available in warehouse {1},El lote {0} no está disponible en el almacén {1}, +Batchwise Valuation,Valoración por lotes, +Beginning of the current subscription period,Inicio del periodo de suscripción actual, +Below Subscription Plans are of different currency to the party default billing currency/Company currency: {0},Los siguientes planes de suscripción tienen una moneda diferente a la moneda de facturación predeterminada de la parte/moneda de la empresa: {0}, +Bill for Rejected Quantity in Purchase Invoice,Facturación de la cantidad rechazada en la factura de compra, +Billed Items To Be Received,Artículos facturados por recibir, +"Billed, Received & Returned","Facturado, Recibido y Devuelto", +Billing Address Details,Detalles de la dirección de facturación, +Billing Interval in Subscription Plan must be Month to follow calendar months,El intervalo de facturación en el plan de suscripción debe ser Mes para seguir los meses naturales, +Bisect Accounting Statements,Estados contables divididos, +Bisect Left,Dividir a la izquierda, +Bisect Nodes,Dividir nodos, +Bisect Right,Dividir a la derecha, +Bisecting From,Dividir desde, +Bisecting Left ...,Dividir a la izquierda..., +Bisecting Right ...,Dividir a la derecha..., +Bisecting To,Dividir a, +Blanket Order Allowance (%),Asignación de pedidos generales (%), +Bom No,Lista de materiales Nº, +Book Advance Payments as Liability option is chosen. Paid From account changed from {0} to {1}.,Se eligió la opción de Reservar pagos por adelantado como pasivo. La cuenta Pagado desde cambió de {0} a {1}., +Book Advance Payments in Separate Party Account,Contabilizar los pagos anticipados en una cuenta separada, +Book Tax Loss on Early Payment Discount,Pérdida de impuestos sobre descuentos de pago anticipado, +Book an appointment,Concierte una cita, +Booking stock value across multiple accounts will make it harder to track stock and account value.,Registrar el valor de las existencias en varias cuentas hará que sea más difícil realizar un seguimiento del valor de las existencias y de las cuentas., +Books have been closed till the period ending on {0},Los libros estarán cerrados hasta el período que finaliza el {0}, +Both Payable Account: {0} and Advance Account: {1} must be of same currency for company: {2},Tanto la Cuenta de Acreedores: {0} como la Cuenta de Anticipos: {1} deben ser de la misma moneda para la empresa: {2}, +Both Receivable Account: {0} and Advance Account: {1} must be of same currency for company: {2},Tanto la cuenta de deudores: {0} como la cuenta de anticipos: {1} deben ser de la misma moneda para la empresa: {2}, +Both {0} Account: {1} and Advance Account: {2} must be of same currency for company: {3},Tanto la cuenta {0} : {1} como la cuenta de anticipos: {2} deben ser de la misma moneda para la empresa: {3}, +Budget Exceeded,Presupuesto excedido, +Build All?,¿Construir todo?, +Build Tree,Construir árbol, +Buildable Qty,Cantidad fabricable, +Bulk Transaction Log,Registro de transacciones masivas, +Bulk Transaction Log Detail,Detalle del registro de transacciones masivas, +Bulk Update,Actualización masiva, +Bundle Items,Conjunto de Productos, +Buying & Selling Settings,Configuración de Compra y Venta, +Buying and Selling,Compra y Venta, +"By default, the Supplier Name is set as per the Supplier Name entered. If you want Suppliers to be named by a Naming Series choose the 'Naming Series' option.","Por defecto, el Nombre del Proveedor se establece según el Nombre del Proveedor introducido. Si desea que los Proveedores sean nombrados por una Serie de Nombres elija la opción 'Serie de Nombres'.", +Bypass credit check at Sales Order,Omitir verificación de crédito en Orden de Venta, +COGS By Item Group,CMV grupo de artículos, +COGS Debit,Débito CMV, +CRM Note,Nota CRM, +CRM Settings,Configuración CRM, +Calculate Product Bundle Price based on Child Items' Rates,Calcular el precio del paquete de productos en función de las tarifas de los artículos secundarios, +Calculate daily depreciation using total days in depreciation period,Calcular la depreciación diaria utilizando el total de días del período de depreciación, +Call Again,Volver a llamar, +Call Ended,Llamada finalizada, +Call Handling Schedule,Horario de atención de llamadas, +Call Received By,Llamada recibida por, +Call Receiving Device,Dispositivo receptor de llamadas, +Call Routing,Enrutamiento de llamadas, +Call Schedule Row {0}: To time slot should always be ahead of From time slot.,Horario de llamadas Fila {0}: La franja horaria A debe estar siempre por delante de la franja horaria Desde., +Call Type,Tipo de llamada, +Callback,Devolver Llamada, +Campaign Item,Artículos de campaña, +Can not close Work Order. Since {0} Job Cards are in Work In Progress state.,No se puede cerrar la Orden de Trabajo. Ya que {0} Las fichas de trabajo están en estado Trabajo en curso., +"Can not filter based on Child Account, if grouped by Account",No se puede filtrar basado en la cuenta secundaria si está agrupada por cuenta, +"Can't change the valuation method, as there are transactions against some items which do not have its own valuation method","No se puede cambiar el método de valoración, ya que hay transacciones contra algunos artículos que no tienen su propio método de valoración.", +Can't disable batch wise valuation for active batches.,No se puede deshabilitar la valoración por lotes para lotes activos., +Can't disable batch wise valuation for items with FIFO valuation method.,No se puede desactivar la valoración por lotes para artículos con método de valoración FIFO., +Cannot Merge,No se puede fusionar, +Cannot Resubmit Ledger entries for vouchers in Closed fiscal year.,No se pueden volver a validar entradas del libro mayor para comprobantes en un año fiscal cerrado., +"Cannot amend {0} {1}, please create a new one instead.","No se puede modificar {0} {1}; en su lugar, cree uno nuevo.", +Cannot apply TDS against multiple parties in one entry,No se puede aplicar Retención de impuestos en origen contra varias partes en una sola entrada, +Cannot cancel as processing of cancelled documents is pending.,No se puede cancelar porque el procesamiento de los documentos cancelados está pendiente., +Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet.,"No se puede cancelar la transacción. La validación del traspaso de la valoración del artículo, aún no se ha completado.", +Cannot change Reference Document Type.,No se puede cambiar el tipo de documento de referencia., +Cannot complete task {0} as its dependant task {1} are not completed / cancelled.,No se puede completar la tarea {0} porque su tarea dependiente {1} no está completada / cancelada., +Cannot convert Task to non-group because the following child Tasks exist: {0}.,No se puede convertir una tarea a una no grupal porque existen las siguientes tareas secundarias: {0}., +Cannot convert to Group because Account Type is selected.,No se puede convertir a Grupo porque Tipo de Cuenta está seleccionado., +Cannot create Stock Reservation Entries for future dated Purchase Receipts.,No se pueden crear entradas de reserva de stock para recibos de compra con fecha futura., +Cannot create a pick list for Sales Order {0} because it has reserved stock. Please unreserve the stock in order to create a pick list.,No se puede crear una lista de selección para la orden de venta {0} porque tiene stock reservado. Anule la reserva del stock para crear una lista de selección., +Cannot create accounting entries against disabled accounts: {0},No se pueden crear asientos contables contra cuentas desactivadas: {0}, +Cannot disable batch wise valuation for FIFO valuation method.,No se puede desactivar la valoración por lotes para el método de valoración FIFO., +Cannot enqueue multi docs for one company. {0} is already queued/running for company: {1},No se pueden poner en cola varios documentos para una empresa. {0} ya está en cola/en ejecución para la empresa: {1}, +Cannot find a default warehouse for item {0}. Please set one in the Item Master or in Stock Settings.,No se puede encontrar un almacén predeterminado para el artículo {0}. Establezca uno en el Maestro de artículos o en la Configuración de existencias., +Cannot make any transactions until the deletion job is completed,No se puede realizar ninguna transacción hasta que se complete el trabajo de eliminación., +Cannot produce more item for {0},No se puede producir más productos por {0}, +Cannot produce more than {0} items for {1},No se pueden producir más de {0} productos por {1}, +Cannot receive from customer against negative outstanding,No se puede recibir del cliente contra saldos pendientes negativos, +Cannot retrieve link token for update. Check Error Log for more information,No se puede recuperar el token de enlace para la actualización. Consulte el registro de errores para obtener más información, +Cannot retrieve link token. Check Error Log for more information,No se puede recuperar el token de enlace. Compruebe el registro de errores para obtener más información, +Cannot {0} from {2} without any negative outstanding invoice,No se puede {0} desde {2} sin ninguna factura pendiente negativa, +Capacity (Stock UOM),Capacidad (Stock UdM), +Capacity in Stock UOM,Capacidad en stock UdM, +Capacity must be greater than 0,La capacidad debe ser superior a 0, +Capitalization,Capitalización, +Capitalization Method,Método de Capitalización, +Capitalize Asset,Capitalizar Activo, +Capitalize Repair Cost,Capitalizar el coste de reparación, +Capitalized,Capitalizado, +Carrier,Operador, +Carrier Service,Servicio de Operador, +Carry Forward Communication and Comments,Llevar adelante la comunicación y los comentarios, +Category Details,Detalles de la categoría, +Caution: This might alter frozen accounts.,Precaución: Esto podría alterar las cuentas congeladas., +Change in Stock Value,Cambio en el Valor de Stock, +Changed customer name to '{}' as '{}' already exists.,Se cambió el nombre del Cliente a '{}' porque '{}' ya existe., +Changes,Cambio, +Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount,El cargo de tipo 'Real' en la fila {0} no puede incluirse en la Tarifa del artículo o en el Importe pagado, +Chart Of Accounts,Plan de Cuentas, +Checked On,Comprobado el, +Checking this will round off the tax amount to the nearest integer,Al marcar esta casilla se redondeará el importe del impuesto al número entero más próximo, +Cheques and Deposits Incorrectly cleared,Cheques y depósitos compensados incorrectamente, +Choose a WIP composite asset,Elija un activo compuesto WIP, +Clear Demo Data,Borrar datos de demostración, +Clear Notifications,Borrar Notificaciones, +Clearing Demo Data...,Borrando datos de demostración..., +Click on 'Get Finished Goods for Manufacture' to fetch the items from the above Sales Orders. Items only for which a BOM is present will be fetched.,"Haga clic en ""Obtener Productos Terminados para Fabricación"" para obtener los artículos de los Pedidos de Ventas anteriores. Sólo se obtendrán los artículos para los que exista una lista de materiales.", +Click on Add to Holidays. This will populate the holidays table with all the dates that fall on the selected weekly off. Repeat the process for populating the dates for all your weekly holidays,Haga clic en Añadir a vacaciones. Esto rellenará la tabla de días festivos con todas las fechas que caen en el día festivo semanal seleccionado. Repita el proceso para rellenar las fechas de todas sus vacaciones semanales, +Click on Get Sales Orders to fetch sales orders based on the above filters.,Haga clic en Obtener pedidos de venta para obtener los pedidos de venta basados en los filtros anteriores., +Click to add email / phone,Clic para añadir correo / teléfono, +Close Replied Opportunity After Days,Cerrar oportunidad respondida después de días, +Closed Work Order can not be stopped or Re-opened,La orden de trabajo cerrada no puede detenerse ni reabrirse, +Closing,Cierre, +Closing Balance as per Bank Statement,Saldo de cierre según extracto bancario, +Closing Balance as per ERP,Saldo de cierre según ERP, +Closing Stock Balance,Saldo de stock de cierre, +Columns are not according to template. Please compare the uploaded file with standard template,"Las columnas no se ajustan a la plantilla. Por favor, compare el archivo subido con la plantilla estándar", +Communication Channel,Canal de comunicación, +Company Address Display,Mostrar dirección de la empresa, +Company Billing Address,Dirección de Facturación de la Compañía, +Company Details,Detalles de la Compañía, +Company Shipping Address,Dirección de envío de la compañía, +Company Tax ID,Número de Identificación Fiscal de la Compañía, +Company and Posting Date is mandatory,La Empresa y la Fecha de Publicación son obligatorias, +Company is mandatory,La empresa es obligatoria, +Company is mandatory for generating an invoice. Please set a default company in Global Defaults.,La empresa es obligatoria para generar una factura. Establezca una empresa predeterminada en Valores predeterminados globales., +Company which internal customer represents,Compañía a la que representa el Cliente Interno, +Company which internal customer represents.,Compañía a la que representa el Cliente Interno., +Company which internal supplier represents,Empresa a la que representa el proveedor interno, +Company {0} is added more than once,La empresa {0} se agrega más de una vez, +Company {} does not exist yet. Taxes setup aborted.,La empresa {} aún no existe. Configuración de impuestos abortada., +Company {} does not match with POS Profile Company {},La empresa {} no coincide con el perfil de POS {}, +Competitor,Competidor, +Competitor Detail,Detalle del Competidor, +Competitor Name,Nombre del Competidor, +Competitors,Competidores, +Complete Job,Trabajo completo, +Complete Order,Pedido completo, +Completed On,Completado el, +Completed On cannot be greater than Today,Completado el no puede ser después de hoy, +Completed Tasks,Tareas Completadas, +Completed Time,Tiempo completado, +Completion Date can not be before Failure Date. Please adjust the dates accordingly.,La fecha de finalización no puede ser anterior a la fecha de falla. Ajuste las fechas según corresponda., +Conditional Rule,Regla condicional, +Conditional Rule Examples,Ejemplos de reglas condicionales, +Configure Product Assembly,Configurar el ensamblaje del producto, +Configure the action to stop the transaction or just warn if the same rate is not maintained.,Configure la acción para detener la transacción o simplemente avisar si no se mantiene la misma tasa., +Connections,Conexiones, +Consider Entire Party Ledger Amount,Considerar el importe total del Libro Mayor de Partes, +Consider Minimum Order Qty,Considerar la cantidad mínima de pedido, +Consider Rejected Warehouses,Considerar los almacenes rechazados, +Considered In Paid Amount,Considerado en el importe pagado, +Consolidate Sales Order Items,Consolidar posiciones de pedido de cliente, +Consolidate Sub Assembly Items,Consolidar elementos de subensamblado, +Consumed Asset Items is mandatory for Decapitalization,Los elementos de activos consumidos son obligatorios para la descapitalización, +Consumed Asset Total Value,Valor total de los activos consumidos, +Consumed Assets,Activos consumidos, +Consumed Quantity,Calidad consumida, +Consumed Stock Items,Artículos de stock consumidos, +Consumed Stock Items or Consumed Asset Items are mandatory for creating new composite asset,Los artículos de stock consumidos o los artículos de activos consumidos son obligatorios para crear un nuevo activo compuesto., +"Consumed Stock Items, Consumed Asset Items or Consumed Service Items is mandatory for Capitalization","Los artículos de stock consumidos, los artículos de activos consumidos o los artículos de servicios consumidos son obligatorios para la capitalización", +Consumed Stock Total Value,Valor total del stock consumido, +Consumption Rate,Tasa de consumo, +Contact Details,Detalles de contacto, +Contact Mobile,Contacto Móvil, +Contact Us Settings,Configuración de contácto, +Contacts,Contactos, +Contract Template Help,Ayuda con la plantilla de contrato, +Contribution Qty,Contribución Cantidad, +Control Historical Stock Transactions,Control de las transacciones históricas de existencias, +Conversion factor for item {0} has been reset to 1.0 as the uom {1} is same as stock uom {2}.,El factor de conversión para el artículo {0} se ha restablecido a 1.0 ya que la unidad de medida {1} es la misma que la unidad de medida de stock {2}., +Convert Item Description to Clean HTML in Transactions,Convertir la descripción del artículo a HTML limpio en las transacciones, +Convert to Group,Convertir a grupo,Warehouse +Convert to Item Based Reposting,Convertir a reenvío basado en artículos, +Convert to Ledger,Convertir a libro mayor,Warehouse +Core,Núcleo, +Corrective Job Card,Ficha de trabajo correctivo, +Corrective Operation,Operación correctiva, +Corrective Operation Cost,Coste de la operación correctiva, +Cost Center Allocation,Asignación de centros de costes, +Cost Center Allocation Percentage,Porcentaje de Asignación del Centro de Costos, +Cost Center Allocation Percentages,Porcentajes de Asignación del Centro de Costo, +Cost Center For Item with Item Code {0} has been Changed to {1},El centro de costes para el artículo con código de artículo {0} se ha cambiado a {1}, +"Cost Center is a part of Cost Center Allocation, hence cannot be converted to a group","El centro de costes forma parte de la asignación de centros de costes, por lo que no puede convertirse en un grupo", +Cost Center with Allocation records can not be converted to a group,El centro de costes con registros de asignación no puede convertirse en un grupo, +Cost Center {0} cannot be used for allocation as it is used as main cost center in other allocation record.,"El centro de costes {0} no puede utilizarse para la asignación, ya que se utiliza como centro de costes principal en otro registro de asignación.", +Cost Center {} doesn't belong to Company {},Centro de costos {} no pertenece a la empresa {}, +Cost Center {} is a group cost center and group cost centers cannot be used in transactions,El centro de costes {} es un centro de costes de grupo y los centros de costes de grupo no pueden utilizarse en las transacciones, +Cost Configuration,Configuración de costes, +Cost Per Unit,Coste por unidad, +Cost of New Capitalized Asset,Coste del nuevo activo capitalizado, +Cost of Poor Quality Report,Informe sobre el coste de la mala calidad, +Cost to Company (CTC),Coste para la empresa (CTE), +Costing Details,Detalles de costos, +Could Not Delete Demo Data,No se pueden borrar los datos de la demostración, +Could not auto update shifts. Shift with shift factor {0} needed.,No se pudieron actualizar automáticamente los turnos. Se necesita un turno con un factor de turno {0} ., +Could not detect the Company for updating Bank Accounts,No se ha podido detectar la empresa para actualizar las cuentas bancarias, +Could not find path for ,No se pudo encontrar la ruta para , +Count,Contar, +Create Depreciation Entry,Crear asiento de amortización, +Create Employee records.,Crear registros de empleados., +Create Grouped Asset,Crear activos agrupados, +Create Job Card based on Batch Size,Crear tarjeta de trabajo en función del tamaño del lote, +Create Journal Entries,Crear asientos, +Create Ledger Entries for Change Amount,Crear entradas en el libro mayor para el importe de modificación, +Create Link,Crear enlace, +Create Multi-level BOM,Crear lista de materiales Multi-Nivel, +Create New Customer,Crear Nuevo Cliente, +Create Opportunity,Crear Oportunidad, +Create Prospect,Crear prospecto, +Create Reposting Entries,Crear entradas de reenvío, +Create Reposting Entry,Crear entrada de reenvío, +Create Stock Entry,Crear entrada de stock, +Create Workstation,Crear estación de trabajo, +Create a new composite asset,Crear un nuevo activo compuesto, +Create a variant with the template image.,Cree una variante con la imagen de la plantilla., +Create in Draft Status,Crear en estado Borrador, +Create {0} {1} ?,¿Crear {0} {1} ?, +Created On,Creado el, +Created {0} scorecards for {1} between:,Se crearon {0} tarjetas de puntos para {1} entre:, +Creating Delivery Note ...,Creando Nota de Entrega..., +Creating Journal Entries...,Creación de asientos de diario..., +Creating Packing Slip ...,Creando albarán..., +Creating Purchase Invoices ...,Creando facturas de compra..., +Creating Purchase Receipt ...,Creando Recibo de Compra..., +Creating Sales Invoices ...,Creando facturas de venta..., +Creating Stock Entry,Creando Entrada de Inventario, +Creating Subcontracting Order ...,Creando Orden de Subcontratación..., +Creating Subcontracting Receipt ...,Creando Recibo de Subcontratación..., +Creating User...,Creando usuario..., +Creation,Creación, +Creation of {1}(s) successful,Creación de {1}(s) exitosa, +"Creation of {0} failed. + Check Bulk Transaction Log","La creación de {0} falló. + Verificar Registro de transacciones masivas", +"Creation of {0} partially successful. + Check Bulk Transaction Log","Creación de {0} parcialmente satisfactoria. + Compruebe Registro de transacciones masivas", +Credit (Transaction),Crédito (Transacción), +Credit Amount in Transaction Currency,Importe del crédito en la moneda de la transacción, +Credit Limit Crossed,Límite de crédito sobrepasado, +Credit Limit Settings,Configuración del límite de crédito, +"Credit Note will update it's own outstanding amount, even if ""Return Against"" is specified.","La nota de crédito actualizará su propio importe pendiente, incluso si se especifica ""Devolución contra"".", +Currency Exchange Settings Details,Detalles de la configuración del cambio de divisas, +Currency Exchange Settings Result,Resultado de la configuración de cambio de moneda, +Current Asset,Activo corriente, +Current Index,Índice actual, +Current Level,Nivel actual, +Current Liability,Pasivo corriente, +Current Node,Nodo actual, +Current Serial / Batch Bundle,Paquete de serie / lote actual, +Custom,Personalizar, +Custom delimiters,Delimitador personalizado, +Customer ,Cliente , +Customer / Item / Item Group,Cliente / Producto / Grupo de Productos, +Customer Defaults,Valores predeterminados del cliente, +Customer Group Item,Artículo del grupo de clientes, +Customer Group: {0} does not exist,Grupo de Clientes: {0} no existe, +Customer Item,Artículo del cliente, +Customer Name: ,Nombre del cliente: , +Customer Portal Users,Usuarios del Portal del Cliente, +Customer: ,Cliente: , +Daily Time to send,Tiempo diario para enviar, +Dashboard,Tablero, +Data Based On,Datos basados en, +Date ,Fecha , +Date must be between {0} and {1},La fecha debe estar entre {0} y {1}, +Days before the current subscription period,Días antes del período de suscripción actual, +DeLinked,Desvinculado, +Deal Owner,Propietario de la Oferta, +Debit (Transaction),Débito (Transacción), +Debit Amount in Transaction Currency,Importe del débito en la moneda de la transacción, +"Debit Note will update it's own outstanding amount, even if ""Return Against"" is specified.","La nota de débito actualizará su propio monto pendiente, incluso si se especifica ""Devolver contra"".", +Debit-Credit Mismatch,Desajuste débito-crédito, +Debit-Credit mismatch,Desajuste débito-crédito, +Decapitalization,Descapitalización, +Decapitalized,Descapitalizado, +Default Advance Account,Cuenta de anticipos por defecto, +Default Advance Paid Account,Cuenta de anticipos por defecto, +Default Advance Received Account,Cuenta de anticipos recibidos por defecto, +Default BOM not found for FG Item {0},Lista de materiales por defecto no encontrada para el artículo FG {0}, +Default Discount Account,Cuenta de descuento predeterminada, +Default In-Transit Warehouse,Almacén en tránsito predeterminado, +Default Operating Cost Account,Cuenta de costos operativos por defecto, +Default Payment Discount Account,Cuenta de descuento por pago predeterminado, +Default Provisional Account,Cuenta provisional predeterminada, +Default Service Level Agreement for {0} already exists.,Ya existe un acuerdo de nivel de servicio predeterminado para {0} ., +Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You need to either cancel the linked documents or create a new Item.,La unidad de medida predeterminada para el artículo {0} no se puede cambiar directamente porque ya ha realizado alguna transacción con otra unidad de medida. Debe cancelar los documentos vinculados o crear un artículo nuevo., +Default settings for your stock-related transactions,Configuración predeterminada para sus transacciones relacionadas con acciones, +"Default tax templates for sales, purchase and items are created.","Se crean plantillas de impuestos por defecto para ventas, compras y artículos.", +Deferred Accounting,Contabilidad diferida, +Deferred Accounting Defaults,Contabilidad diferida predeterminada, +Deferred Revenue and Expense,Ingresos y gastos diferidos, +Deferred accounting failed for some invoices:,La contabilidad diferida falló para algunas facturas:, +Delay (In Days),Retraso (en días), +Delayed,Retrasado, +Delayed Tasks Summary,Resumen de tareas retrasadas, +Delete Accounting and Stock Ledger Entries on deletion of Transaction,Borrar asientos del libro de contabilidad y de existencias al borrar una transacción, +Delete Bins,Eliminar contenedores, +Delete Cancelled Ledger Entries,Eliminar entradas contables canceladas, +Delete Dimension,Eliminar Dimensión, +Delete Leads and Addresses,Eliminar clientes potenciales y direcciones, +Delete Transactions,Eliminar transacciones, +Deleted Documents,Documentos Eliminados, +Deletion in Progress!,¡Eliminación en progreso!, +Delimiter options,Opciones de delimitador, +Delivery Manager,Gerente de Envío, +Delivery Note Packed Item,Albarán de entrega Artículo embalado, +Delivery Note(s) created for the Pick List,Nota(s) de entrega creada(s) para la lista de selección, +Delivery User,Usuario de Envío, +Delivery to,Entregar a, +Demo Company,Empresa de Demostración, +Demo data cleared,Datos de demostración borrados, +Dependant SLE Voucher Detail No,Detalle del comprobante SLE dependiente N.º, +Dependent Task {0} is not a Template Task,La tarea dependiente {0} no es una tarea plantilla, +Deposit,Depósito, +Depreciate based on daily pro-rata,Depreciación basada en el prorrateo diario, +Depreciate based on shifts,Depreciar según turnos, +Depreciation Details,Detalles de la depreciación, +Depreciation Entry Posting Status,Estado de contabilización del asiento de amortización, +Depreciation Expense Account should be an Income or Expense Account.,La cuenta de gastos de depreciación debe ser una cuenta de ingresos o de gastos., +Depreciation Posting Date cannot be before Available-for-use Date,La fecha de contabilización de la depreciación no puede ser anterior a la fecha de disponibilidad para uso, +Depreciation Row {0}: Depreciation Posting Date cannot be before Available-for-use Date,Fila de depreciación {0}: La fecha de contabilización de la depreciación no puede ser anterior a la fecha de disponibilidad para uso, +Depreciation Schedule View,Vista del calendario de amortización, +Depreciation cannot be calculated for fully depreciated assets,La amortización no puede calcularse para los activos totalmente amortizados, +Description of Content,Descripción del contenido, +Desk User,Usuario de Escritorio, +Difference In,Diferencia en, +Difference Posting Date,Fecha de publicación de la diferencia, +Difference Qty,Diferencia Cant., +Different 'Source Warehouse' and 'Target Warehouse' can be set for each row.,Se pueden configurar diferentes 'Almacén de origen' y 'Almacén de destino' para cada fila., +Dimension Details,Detalles de la dimensión, +Dimension Filter Help,Ayuda del filtro de dimensiones, +Dimension-wise Accounts Balance Report,Informe de balance de cuentas por dimensiones, +Direct Expense,Gastos Directos, +Disable Last Purchase Rate,Desactivar última tasa de compra, +Disable Serial No And Batch Selector,Desactivar selección de Nún. de Serie y Lote, +Disabled Account Selected,Cuenta deshabilitada seleccionada, +Disabled Warehouse {0} cannot be used for this transaction.,El almacén deshabilitado {0} no se puede utilizar para esta transacción., +Disabled pricing rules since this {} is an internal transfer,Deshabilitado las reglas de precios ya que esta {} es una transferencia interna, +Disabled tax included prices since this {} is an internal transfer,"Precios con impuestos incluidos, ya que este {} es un traslado interno", +Disables auto-fetching of existing quantity,Desactiva la obtención automática de la cantidad existente, +Disassemble,Desmontar, +Disassemble Order,Orden de desmontaje, +Discount Account,Cuenta de Descuento, +Discount Date,Fecha de descuento, +Discount Settings,Configuración de Descuento, +Discount Validity,Validez del descuento, +Discount Validity Based On,Validez del descuento basado en, +Discount of {} applied as per Payment Term,Descuento de {} aplicado según la Condición de Pago, +Discounted Amount,Importe descontado, +"Discounts to be applied in sequential ranges like buy 1 get 1, buy 2 get 2, buy 3 get 3 and so on","Descuentos a aplicar en rangos secuenciales como compra 1 consigue 1, compra 2 consigue 2, compra 3 consigue 3 y así sucesivamente", +Discrepancy between General and Payment Ledger,Discrepancia entre el Libro Mayor y el Libro de Pagos, +Dispatch Address,Dirección de Despacho, +Dispatch Address Name,Nombre de Dirección de Despacho, +Distinct Item and Warehouse,Distinto artículo y almacén, +Distribute Additional Costs Based On ,Distribuir los costes adicionales en función de , +Distribute Manually,Distribuir manualmente, +Do Not Explode,No desglosar, +Do Not Update Serial / Batch on Creation of Auto Bundle,No actualizar el número de serie o lote al crear un paquete automático, +Do Not Use Batch-wise Valuation,No utilice la valoración por lotes, +Do reposting for each Stock Transaction,Realice el traspaso para cada transacción bursátil, +Do you still want to enable negative inventory?,¿Aún desea activar el inventario negativo?, +DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it.,Los DocTypes no deben añadirse manualmente a la tabla 'DocTypes excluidos'. Solamente se le permite eliminar entradas de la misma., +Document Type already used as a dimension,Tipo de documento ya utilizado como dimensión, +Documents,Documentos, +Documents: {0} have deferred revenue/expense enabled for them. Cannot repost.,Documentos: {0} tienen habilitados ingresos/gastos diferidos. No se pueden volver a publicar., +Domain Settings,Configuraciones de Dominio, +Don't Reserve Sales Order Qty on Sales Return,No reserve la cantidad de la orden de venta en la devolución de ventas, +Don't Send Emails,No envíe correos electrónicos, +Dont Recompute tax,No recalcular impuestos, +Download Backups,Descargar Backups, +Download CSV Template,Descargar la plantilla CSV, +Download Materials Request Plan,Descargar Plan de Solicitud de Materiales, +Download Materials Request Plan Section,Descargar Sección de Solicitud de Planos de Materiales, +Dunning Amount (Company Currency),Importe de la reclamación (moneda de la empresa), +Dunning Level,Nivel de reclamación, +Duplicate Closing Stock Balance,Saldo de stock de cierre duplicado, +Duplicate Customer Group,Grupo de clientes duplicados, +Duplicate Finance Book,Duplicado del Libro de Finanzas, +Duplicate Item Group,Grupo de Productos duplicado, +Duplicate POS Invoices found,Se encontraron facturas PV duplicadas, +Dynamic Condition,Condición Dinámica, +Edit Capacity,Editar capacidad, +Edit Cart,Editar carrito, +Edit Full Form,Editar formulario completo, +Edit Note,Editar Nota, +Editing {0} is not allowed as per POS Profile settings,La edición de {0} no está permitida según la configuración del perfil del PV, +Either 'Selling' or 'Buying' must be selected,"Debe seleccionar ""Vender"" o ""Comprar"".", +Email / Notifications,Correo / Notificaciones, +Email Address (required),Dirección de correo electrónico (obligatorio), +"Email Address must be unique, it is already used in {0}","La dirección de correo electrónico debe ser única, ya se utiliza en {0}", +Email Digest Recipient,Destinatario del resumen de correo electrónico, +Email Digest: {0},Resumen de correo: {0}, +Email Domain,Dominio de Correo Electrónico, +Email or Phone/Mobile of the Contact are mandatory to continue.,El correo electrónico o el teléfono/móvil del contacto son obligatorios para continuar., +Email verification failed.,Error en la verificación del correo electrónico., +Employee User Id,Id. de usuario del empleado, +Enable Allow Partial Reservation in the Stock Settings to reserve partial stock.,Habilite Permitir reserva parcial en la configuración de stock para reservar stock parcial., +Enable Automatic Party Matching,Habilitar la coincidencia automática de partes, +Enable Common Party Accounting,Habilitar la contabilidad de partes comunes, +Enable Discount Accounting for Selling,Habilitar la contabilidad de descuentos para las ventas, +Enable Fuzzy Matching,Habilitar coincidencia difusa, +Enable Health Monitor,Habilitar monitor de salud, +Enable Immutable Ledger,Habilitar libro mayor inmutable, +Enable Provisional Accounting For Non Stock Items,Habilitar contabilidad provisional para artículos que no están en stock, +Enable Stock Reservation,Habilitar reserva de existencias, +Enable it if users want to consider rejected materials to dispatch.,Habilítelo si los usuarios desean considerar los materiales rechazados para enviarlos., +Enable this checkbox even if you want to set the zero priority,Active esta casilla incluso si desea establecer la prioridad cero, +"Enable this option to calculate daily depreciation by considering the total number of days in the entire depreciation period, (including leap years) while using daily pro-rata based depreciation","Active esta opción para calcular la amortización diaria teniendo en cuenta el número total de días de todo el período de amortización, (incluidos los años bisiestos) al utilizar la amortización diaria prorrateada.", +Enable to apply SLA on every {0},Habilitar para aplicar SLA en cada {0}, +Enabling this ensures each Purchase Invoice has a unique value in Supplier Invoice No. field within a particular fiscal year,Habilitar esta opción garantiza que cada factura de compra tenga un valor único en el campo Nº de factura del proveedor dentro de un ejercicio fiscal determinado, +Enabling this option will allow you to record -

1. Advances Received in a Liability Account instead of the Asset Account

2. Advances Paid in an Asset Account instead of the Liability Account,Activar esta opción le permitirá registrar -

1. Anticipos Recibidos en una Cuenta de Pasivo en lugar de la Cuenta de Activo

2. Anticipos Pagados en una Cuenta de Activo en lugar de la Cuenta de Pasivo, +Enabling this will allow creation of multi-currency invoices against single party account in company currency,Habilitar esta opción permitirá la creación de facturas multidivisa contra la cuenta de una sola parte en la divisa de la empresa, +Enabling this will change the way how cancelled transactions are handled.,Al activar esta opción cambiará la forma en que se gestionan las transacciones canceladas., +End Transit,Fin del tránsito, +End of the current subscription period,Fin del periodo de suscripción actual, +"Enter First and Last name of Employee, based on Which Full Name will be updated. IN transactions, it will be Full Name which will be fetched.","Introduzca el Nombre y Apellidos del Empleado, en base a los cuales se actualizará el Nombre Completo. EN las transacciones, será el Nombre Completo el que se obtendrá.", +Enter Manually,Introducir manualmente, +Enter Serial Nos,Ingrese Serial Nro., +Enter Visit Details,Introduzca los datos de la visita, +Enter a name for Routing.,Introduzca un nombre para el Enrutamiento., +"Enter a name for the Operation, for example, Cutting.","Introduzca un nombre para la Operación, por ejemplo, Corte.", +Enter a name for this Holiday List.,Introduzca un nombre para esta Lista de vacaciones., +"Enter an Item Code, the name will be auto-filled the same as Item Code on clicking inside the Item Name field.","Introduzca un Código de Artículo, el nombre se autocompletará igual que Código de Artículo al pulsar dentro del campo Nombre de Artículo.", +Enter each serial no in a new line,Introduzca cada nº de serie en una nueva línea, +"Enter the Operation, the table will fetch the Operation details like Hourly Rate, Workstation automatically. + + After that, set the Operation Time in minutes and the table will calculate the Operation Costs based on the Hourly Rate and Operation Time.","Introduzca la Operación, la tabla obtendrá los detalles de la Operación como la Tasa Horaria, la Estación de Trabajo automáticamente. + + Después, fije el Tiempo de Operación en minutos y la tabla calculará los Costes de Operación basándose en la Tarifa Horaria y el Tiempo de Operación.", +Enter the opening stock units.,Introduzca las unidades de existencias iniciales., +Enter the quantity of the Item that will be manufactured from this Bill of Materials.,Introduzca la cantidad del Artículo que se fabricará a partir de esta Lista de Materiales., +Enter the quantity to manufacture. Raw material Items will be fetched only when this is set.,Introduzca la cantidad a fabricar. Los artículos de materia prima sólo se obtendrán cuando se haya configurado esta opción., +Error during caller information update,Error al actualizar la información de llamada, +Error while posting depreciation entries,Error al contabilizar asientos de amortización, +Error while processing deferred accounting for {0},Error al procesar la contabilidad diferida para {0}, +Error while reposting item valuation,Error al volver a publicar la valoración del artículo, +"Error: This asset already has {0} depreciation periods booked. + The `depreciation start` date must be at least {1} periods after the `available for use` date. + Please correct the dates accordingly.","Error: Este activo ya tiene contabilizados {0} periodos de amortización. + La fecha de `inicio de la amortización` debe ser al menos {1} periodos después de la fecha de `disponible para su uso`. + Por favor, corrija las fechas en consecuencia.", +Errors Notification,Notificación de errores, +Even invoices with apply tax withholding unchecked will be considered for checking cumulative threshold breach,Incluso las facturas que no tengan marcada la opción de aplicar la retención de impuestos se tendrán en cuenta para comprobar el incumplimiento del umbral acumulativo., +Example URL,URL de ejemplo, +Example of a linked document: {0},Ejemplo de documento vinculado: {0}, +"Example: ABCD.##### +If series is set and Serial No is not mentioned in transactions, then automatic serial number will be created based on this series. If you always want to explicitly mention Serial Nos for this item. leave this blank.","Ejemplo: ABCD. #####. Si se establece una serie y no se menciona el No de lote en las transacciones, se creará un número de lote automático basado en esta serie. Si siempre quiere mencionar explícitamente el No de lote para este artículo, déjelo en blanco. Nota: esta configuración tendrá prioridad sobre el Prefijo de denominación de serie en Configuración de stock.", +Example: Serial No {0} reserved in {1}.,Ejemplo: Número de serie {0} reservado en {1}., +Excess Materials Consumed,Exceso de materiales consumidos, +Excess Transfer,Exceso de transferencia, +Exchange Gain Or Loss,Ganancias o pérdidas por cambio de divisas, +Exchange Gain/Loss amount has been booked through {0},El importe de las ganancias/pérdidas de cambio se ha contabilizado a través de {0}., +Exchange Rate Revaluation Settings,Configuración de revaluación del tipo de cambio, +Excluded DocTypes,DocTipes excluidos, +Exempt Supplies,Suministros exentos, +Expected,Esperado, +Expected Balance Qty,Balance esperado, +Expected End Date should be less than or equal to parent task's Expected End Date {0}.,La fecha de finalización esperada debe ser menor o igual a la fecha de finalización esperada de la tarea principal {0}., +Expected Stock Value,Valor esperado de stock, +Expected Time Required (In Mins),Tiempo previsto necesario (en minutos), +Expiry,Expiración, +Export Data,Exportar Datos, +Export Errored Rows,Exportar filas con errores, +Export Import Log,Exportar registro de importación, +Extra Consumed Qty,Cantidad extra consumida, +Extra Job Card Quantity,Cantidad de tarjetas de trabajo adicionales, +FIFO Queue vs Qty After Transaction Comparison,Comparación entre cola FIFO y cantidad después de la transacción, +"FIFO Stock Queue (qty, rate)","Cola de existencias FIFO (cantidad, tasa)", +FIFO/LIFO Queue,Cola FIFO/LIFO, +Failed Entries,Entradas fallidas, +"Failed to erase demo data, please delete the demo company manually.","Fallo al borrar los datos de demostración, por favor borre la empresa de demostración manualmente.", +Failed to post depreciation entries,Fallo al contabilizar las entradas de depreciación, +Failed to setup defaults for country {0}. Please contact support.,Fallo al configurar los valores predeterminados para el país {0}. Póngase en contacto con el servicio de asistencia., +Failure,Fracaso, +Failure Description,Descripción del fallo, +Fetch Based On,Obtener Basado en, +Fetch Overdue Payments,Recuperar pagos atrasados, +Fetch Timesheet,Obtener Hoja de Tiempo, +Fetch Value From,Obtener valor de, +Fetching exchange rates ...,Obteniendo tipos de cambio..., +Filter by Reference Date,Filtrar por Fecha de Referencia, +Filter on Invoice,Filtrar en factura, +Filter on Payment,Filtrar en el pago, +Filters missing,Faltan filtros, +Final Product,Producto final, +Financial Ratios,Índices financieros, +Financial Reports,Informes Financieros, +Financial Year Begins On,El año fiscal comienza el, +Financial reports will be generated using GL Entry doctypes (should be enabled if Period Closing Voucher is not posted for all years sequentially or missing) ,Los informes financieros se generarán utilizando los doctypes de entrada GL (debe activarse si el Comprobante de Cierre de Período no se contabiliza para todos los años secuencialmente o faltantes) , +Finished Good BOM,Lista de materiales de productos terminados, +Finished Good Item,Artículo de Producto Terminado, +Finished Good Item Qty,Producto acabado Cantidad, +Finished Good Item Quantity,Producto acabado Cantidad, +Finished Good Item is not specified for service item {0},Artículo de producto terminado no especificado para artículo de servicio {0}, +Finished Good Item {0} Qty can not be zero,Producto terminado Artículo {0} La cantidad no puede ser cero, +Finished Good Item {0} must be a sub-contracted item,El artículo terminado {0} debe ser un artículo subcontratado, +Finished Good Qty,Cantidad de producto acabado, +Finished Good Quantity ,Cantidad de producto acabado , +Finished Good UOM,Producto acabado UdM, +Finished Good {0} does not have a default BOM.,El Producto acabado {0} no tiene una lista de materiales predeterminada., +Finished Good {0} is disabled.,El Producto acabado {0} está deshabilitado., +Finished Good {0} must be a stock item.,El Producto acabado {0} debe ser un artículo de stock., +Finished Good {0} must be a sub-contracted item.,El producto terminado {0} debe ser un artículo subcontratado., +Finished Goods Based Operating Cost,Coste de explotación basado en los productos acabados, +Finished Goods Item,Producto acabado, +Finished Goods Reference,Productos acabados Referencia, +Finished Goods Value,Valor de los productos acabados, +Finished Goods based Operating Cost,Coste de explotación basado en los productos acabados, +Finished Item {0} does not match with Work Order {1},Artículo terminado {0} no coincide con la orden de trabajo {1}, +First Response Due,Primera respuesta pendiente, +First Response SLA Failed by {},El primer acuerdo de nivel de servicio de respuesta falló por {}, +Fixed Asset Defaults,Cuenta de activo fijo predeterminada, +Fixed Time,Tiempo fijo, +Floor,Piso, +Floor Name,Nombre del Piso, +For Item,Para artículo, +For Item {0} cannot be received more than {1} qty against the {2} {3},Para el artículo {0} no se puede recibir más de {1} cantidad contra {2} {3}, +For Job Card,Para tarjeta de trabajo, +For Operation,Para operaciones, +"For Return Invoices with Stock effect, '0' qty Items are not allowed. Following rows are affected: {0}","Para las Facturas de Devolución con efecto de Stock, no se permiten artículos de cant. '0'. Se ven afectadas las siguientes líneas: {0}", +For Work Order,Para Orden de Trabajo, +For dunning fee and interest,Por gastos de reclamación e intereses, +"For item {0}, rate must be a positive number. To Allow negative rates, enable {1} in {2}","Para el producto {0}, el precio debe ser un número positivo. Para permitir precios negativos, habilite {1} en {2}", +For quantity {0} should not be greater than allowed quantity {1},Para la cantidad {0} no debe ser mayor que la cantidad permitida {1}, +"For the item {0}, the quantity should be {1} according to the BOM {2}.","Para el artículo {0}, la cantidad debe ser {1} según la lista de materiales {2}.", +"For the {0}, no stock is available for the return in the warehouse {1}.","Para la {0}, no hay existencias disponibles para la devolución en el almacén {1}.", +"For the {0}, the quantity is required to make the return entry","Para el {0}, se requiere la cantidad para realizar la entrada de devolución", +Force-Fetch Subscription Updates,Actualizaciones forzadas de suscripciones, +Forecasting,Previsión, +Formula Based Criteria,Criterios basados en fórmulas, +Free Item Rate,Tarifa de artículo libre, +From Corrective Job Card,De la ficha de trabajo correctiva, +From Date and To Date are mandatory,Desde la fecha y hasta la fecha son obligatorios, +From Date is mandatory,Desde Fecha es obligatorio, +From Date: {0} cannot be greater than To date: {1},Desde la fecha: {0} no puede ser mayor que Hasta la fecha: {1}, +From Delivery Date,Desde la fecha de entrega, +From Doctype,Desde DocType, +From Due Date,Desde la fecha de vencimiento, +From Opportunity,Desde Oportunidad, +From Payment Date,Desde la fecha de pago, +From Prospect,Desde Prospecto, +From Reference Date,Desde la fecha de referencia, +From Voucher Detail No,Desde el número de detalle del comprobante, +From Voucher No,Del número de comprobante, +From Voucher Type,Desde el tipo de comprobante, +From and To dates are required,Las fechas desde y hasta son obligatorias, +Full and Final Statement,Declaración completa y definitiva, +GL Balance,Balance GL, +GL Entry Processing Status,Estado de procesamiento de la entrada GL, +GL reposting index,Índice de traspaso GL, +Gain/Loss accumulated in foreign currency account. Accounts with '0' balance in either Base or Account currency,Ganancia/pérdida acumulada en cuenta en moneda extranjera. Cuentas con saldo “0” en moneda base o de cuenta, +Gain/Loss already booked,Ganancias/pérdidas ya contabilizadas, +Gain/Loss from Revaluation,Ganancias/pérdidas por revalorización, +General Ledger,Balance general,Warehouse +General and Payment Ledger Comparison,Comparación de los libros mayor y de pagos, +General and Payment Ledger mismatch,Desajuste entre el Libro Mayor y el Libro de Pagos, +Generate Closing Stock Balance,Generar balance de cierre de stock, +Generate Demo Data for Exploration,Generar datos de demostración para la exploración, +Generate E-Invoice,Generar E-Factura, +Generate Invoice At,Generar factura el, +Generated,Generado, +Generating Preview,Generando vista previa, +Get Allocations,Obtener Asignaciones, +Get Customer Group Details,Obtener Detalles del Grupo de Clientes, +Get Finished Goods for Manufacture,Obtener productos acabados para su fabricación, +Get Outstanding Orders,Obtener pedidos pendientes, +Get Raw Materials Cost from Consumption Entry,Obtenga el coste de las materias primas a partir de la entrada de consumo, +Get Raw Materials for Purchase,Obtener materias primas para comprar, +Get Raw Materials for Transfer,Obtener materias primas para el traslado, +Get Scrap Items,Obtener artículos de desecho, +Get Stock,Obtener existencias, +Get Sub Assembly Items,Obtener artículos de subensamblaje, +Get Supplier Group Details,Obtener detalles del grupo de proveedores, +Get Timesheets,Obtener Hojas de Tiempo, +Get stops from,Obtener paradas de, +Getting Scrap Items,Obtener artículos de desecho, +Give free item for every N quantity,Regale un artículo gratis por cada cantidad N, +Go back,Volver, +Go to {0} List,Ir a la lista {0}, +Goals,Objetivos, +Goods,Mercancías, +Grant Commission,Conceder Comisión, +Greeting Message,Mensaje de saludo, +Gross Profit Percent,Porcentaje de beneficio bruto, +Gross Purchase Amount Too Low: {0} cannot be depreciated over {1} cycles with a frequency of {2} depreciations.,Importe bruto de compra demasiado bajo: {0} no puede amortizarse a lo largo de {1} ciclos con una frecuencia de {2} amortizaciones., +Gross Purchase Amount should be equal to purchase amount of one single Asset.,El monto bruto de compra debe ser igual a al monto de compra de un solo activo., +Group Same Items,Agrupar mismos artículos, +Growth View,Vista de Crecimiento, +Half-yearly,Medio año, +Handle Employee Advances,Gestionar los anticipos de los empleados, +Has Alternative Item,Tiene Ítem Alternativo, +Has Item Scanned,Tiene artículos escaneados, +Has Priority,Tiene prioridad, +Have Default Naming Series for Batch ID?,¿Dispone de series de nombres por defecto para el ID de lote?, +Heatmap,Mapa de calor, +Height (cm),Altura (cm), +"Hello,","Hola,", +Helps you distribute the Budget/Target across months if you have seasonality in your business.,Le ayuda a distribuir el Presupuesto/Objetivo a lo largo de los meses si tiene estacionalidad en su negocio., +Here are the error logs for the aforementioned failed depreciation entries: {0},A continuación se muestran los registros de errores de las entradas de depreciación fallidas mencionadas anteriormente: {0}, +Here are the options to proceed:,Estas son las opciones para proceder:, +"Here, you can select a senior of this Employee. Based on this, Organization Chart will be populated.","Aquí puede seleccionar un superior de este Empleado. Basándose en esto, se rellenará el Organigrama.", +"Here, your weekly offs are pre-populated based on the previous selections. You can add more rows to also add public and national holidays individually.","Aquí, los días libres semanales se rellenan previamente en función de las selecciones anteriores. Puede agregar más filas para agregar también días festivos públicos y nacionales de forma individual.", +"Hi,","Hola,", +Hide Images,Ocultar Imágenes, +Holiday Date {0} added multiple times,Fecha de vacaciones {0} añadida varias veces, +Hours Spent,Horas Dedicadas, +How often should Project be updated of Total Purchase Cost ?,¿Con qué frecuencia debe actualizarse el proyecto del coste total de compra?, +Idle,Inactivo, +"If Enabled - Reconciliation happens on the Advance Payment posting date
+If Disabled - Reconciliation happens on oldest of 2 Dates: Invoice Date or the Advance Payment posting date
+","Si está habilitado : la conciliación se realiza en la fecha de contabilización del pago por adelantado
+Si está deshabilitado : la conciliación se realiza en la fecha más antigua de las 2 fechas: fecha de factura o la fecha de contabilización del pago por adelantado
+", +"If an operation is divided into sub operations, they can be added here.","Si una operación se divide en suboperaciones, éstas pueden añadirse aquí.", +"If checked, Rejected Quantity will be included while making Purchase Invoice from Purchase Receipt.","Si se marca, la Cantidad Rechazada se incluirá al hacer la Factura de Compra a partir del Recibo de Compra.", +"If checked, Stock will be reserved on Submit","Si está marcado, se reservarán las existencias al Validar", +"If checked, picked qty won't automatically be fulfilled on submit of pick list.","Si se marca, la cantidad seleccionada no se completará automáticamente al validar la lista de selección.", +"If checked, the tax amount will be considered as already included in the Paid Amount in Payment Entry","Si está marcada, el importe del impuesto se considerará ya incluido en el Importe pagado en la Entrada de pago", +"If checked, we will create demo data for you to explore the system. This demo data can be erased later.","Si está marcada, crearemos datos de demostración para que explore el sistema. Estos datos de demostración pueden borrarse posteriormente.", +If enabled then system will manufacture Sub-assembly against the Job Card (operation).,"Si se activa, el sistema fabricará el subconjunto según la tarjeta de trabajo (operación).", +If enabled then system won't apply the pricing rule on the delivery note which will be create from the pick list,"Si se activa, el sistema no aplicará la regla de precios en el albarán que se creará a partir de la lista de selección.", +If enabled then system won't override the picked qty / batches / serial numbers.,"Si se activa, el sistema no anulará las cantidades / lotes / números de serie elegidos.", +"If enabled, a print of this document will be attached to each email","Si está habilitado, se adjuntará una impresión de este documento a cada correo electrónico", +"If enabled, additional ledger entries will be made for discounts in a separate Discount Account","Si está habilitado, se realizarán entradas contables adicionales para descuentos en una cuenta de descuento separada", +"If enabled, all files attached to this document will be attached to each email","Si está habilitado, todos los archivos adjuntos a este documento se adjuntarán a cada correo electrónico.", +"If enabled, do not update serial / batch values in the stock transactions on creation of auto Serial + / Batch Bundle. ","Si está habilitado, no actualice los valores de serie/lote en las transacciones de stock al crear automáticamente el paquete de serie + /lote. ", +"If enabled, ledger entries will be posted for change amount in POS transactions","Si se activa, se contabilizarán las entradas en el libro mayor para el importe de cambio en las transacciones de TPV", +"If enabled, the consolidated invoices will have rounded total disabled","Si se activa, las facturas consolidadas tendrán el total redondeado desactivado", +"If enabled, the item rate won't adjust to the valuation rate during internal transfers, but accounting will still use the valuation rate.","Si está activada, la tasa del artículo no se ajustará a la tasa de valoración durante las transferencias internas, pero la contabilidad seguirá utilizando la tasa de valoración.", +"If enabled, the system will create material requests even if the stock exists in the 'Raw Materials Warehouse'.","Si se activa, el sistema creará solicitudes de material aunque haya existencias en el ""Almacén de materias primas"".", +"If enabled, the system will use the moving average valuation method to calculate the valuation rate for the batched items and will not consider the individual batch-wise incoming rate.","Si se activa, el sistema utilizará el método de valoración media móvil para calcular la tasa de valoración de los artículos por lotes y no tendrá en cuenta la tasa de entrada individual por lotes.", +"If enabled, then system will only validate the pricing rule and not apply automatically. User has to manually set the discount percentage / margin / free items to validate the pricing rule","Si está activada, el sistema sólo validará la regla de precios y no la aplicará automáticamente. El usuario debe establecer manualmente el porcentaje de descuento / margen / artículos gratuitos para validar la regla de precios.", +"If mentioned, the system will allow only the users with this Role to create or modify any stock transaction earlier than the latest stock transaction for a specific item and warehouse. If set as blank, it allows all users to create/edit back-dated transactions.","Si se menciona, el sistema permitirá sólo a los usuarios con este Rol crear o modificar cualquier transacción de existencias anterior a la última transacción de existencias para un artículo y almacén específicos. Si se establece en blanco, permite a todos los usuarios crear/editar transacciones con fecha anterior.", +"If not, you can Cancel / Submit this entry","En caso contrario, puedes Cancelar/Validar esta entrada", +"If rate is zero then item will be treated as ""Free Item""","Si la tarifa es cero, el artículo se tratará como ""Artículo gratuito"".", +"If the BOM results in Scrap material, the Scrap Warehouse needs to be selected.","Si la lista de materiales arroja como resultado material de desecho, se debe seleccionar el almacén de desecho.", +"If the selected BOM has Operations mentioned in it, the system will fetch all Operations from BOM, these values can be changed.","Si la lista de materiales seleccionada tiene Operaciones mencionadas en ella, el sistema obtendrá todas las Operaciones de la lista de materiales, estos valores pueden modificarse.", +"If this checkbox is enabled, then the system won’t run the MRP for the available sub-assembly items.","Si esta casilla de verificación está activada, el sistema no ejecutará la planificación de necesidades para los artículos de subensamblaje disponibles.", +If this is undesirable please cancel the corresponding Payment Entry.,"Si no lo desea, anule el asiento de pago correspondiente.", +"If yes, then this warehouse will be used to store rejected materials","En caso afirmativo, este almacén se utilizará para almacenar los materiales rechazados", +"If you are maintaining stock of this Item in your Inventory, ERPNext will make a stock ledger entry for each transaction of this item.","Si mantiene existencias de este artículo en su inventario, ERPNext realizará una entrada en el libro de existencias para cada transacción de este artículo.", +"If you need to reconcile particular transactions against each other, then please select accordingly. If not, all the transactions will be allocated in FIFO order.","Si necesita conciliar transacciones específicas entre sí, seleccione la opción correspondiente. De lo contrario, todas las transacciones se asignarán en orden FIFO.", +"If you still want to proceed, please disable 'Skip Available Sub Assembly Items' checkbox.","Si aún así desea continuar, desactive la casilla 'Omitir elementos de subensamblaje disponibles'.", +"If you still want to proceed, please enable {0}.","Si aún desea continuar, habilite {0}.", +"If your CSV uses a different delimiter, add that character here, ensuring no spaces or additional characters are included.","Si su CSV utiliza un delimitador diferente, añada ese carácter aquí, asegurándose de no incluir espacios ni caracteres adicionales.", +Ignore Account Closing Balance,Ignorar el saldo de cierre de la cuenta, +Ignore Available Stock,Ignorar stock disponible, +Ignore Closing Balance,Ignorar el saldo de cierre, +Ignore Default Payment Terms Template,Ignorar Plantilla de Términos de Pago Predeterminado, +Ignore Empty Stock,Ignorar Stock Vacío, +Ignore Exchange Rate Revaluation Journals,Ignorar los Diarios de Revaluación del Tipo de Cambio, +Ignore Pricing Rule is enabled. Cannot apply coupon code.,La opción Ignorar regla de precios está habilitada. No se puede aplicar el código de cupón., +Ignore System Generated Credit / Debit Notes,Ignorar las notas de crédito / débito generadas por el sistema, +Ignore Voucher Type filter and Select Vouchers Manually,Ignorar el filtro de tipo de cupón y seleccionar cupones manualmente, +Impairment,Deteriorado, +Import File,Importar archivo, +Import File Errors and Warnings,Importar errores y advertencias de archivos, +Import Log Preview,Vista previa de registro de importación, +Import Preview,Vista previa de importación, +Import Progress,Progreso de importación, +Import Type,Tipo de importación, +Import Using CSV file,Importación mediante archivo CSV, +Import Warnings,Advertencias de importación, +Import from Google Sheets,Importar desde Google Sheets, +"Importing {0} of {1}, {2}","Importar {0} de {1}, {2}", +In House,En casa, +In Minutes,En Minutos, +In Party Currency,En moneda del individuo, +In Transit Transfer,Transferencia en tránsito, +In Transit Warehouse,Almacén en Tránsito, +In mins,En minutos, +"In row {0} of Appointment Booking Slots: ""To Time"" must be later than ""From Time"".","En la fila {0} de las franjas horarias de reserva de citas: ""Hora de llegada"" debe ser posterior a ""Hora de salida"".", +"In the case of 'Use Multi-Level BOM' in a work order, if the user wishes to add sub-assembly costs to Finished Goods items without using a job card as well the scrap items, then this option needs to be enable.","En el caso de ""Utilizar lista de materiales multi-nivel"" en una orden de trabajo, si el usuario desea añadir costes de subconjuntos a los artículos de productos acabados sin utilizar una ficha de trabajo, así como a los artículos de desecho, deberá activar esta opción.", +"In this section, you can define Company-wide transaction-related defaults for this Item. Eg. Default Warehouse, Default Price List, Supplier, etc.","En esta sección, puede definir los valores predeterminados relacionados con las transacciones de toda la empresa para este Artículo. Por ejemplo, Almacén por defecto, Lista de precios por defecto, Proveedor, etc.", +Inactive Status,Estado inactivo, +Include Account Currency,Incluir la divisa de la cuenta, +Include Closed Orders,Incluye Pedidos Cerrados, +Include Default FB Assets,Incluir activos FB por defecto, +Include Disabled,Incluye Deshabilitado, +Include Expired Batches,Incluir lotes caducados, +Include Safety Stock in Required Qty Calculation,Incluir stock de seguridad en el cálculo de cantidad requerida, +Include Timesheets in Draft Status,Incluir hojas de horas en estado borrador, +Include Zero Stock Items,Incluir Productos sin existencias, +Incoming Call Handling Schedule,Programa de gestión de llamadas entrantes, +Incoming Call Settings,Configuración de llamadas entrantes, +Incoming Rate (Costing),Tarifa de entrada (costo), +Incorrect Balance Qty After Transaction,Cantidad de saldo incorrecta tras la transacción, +Incorrect Batch Consumed,Lote incorrecto consumido, +Incorrect Check in (group) Warehouse for Reorder,Comprobación incorrecta en (grupo) Almacén para Reordenar, +Incorrect Component Quantity,Cantidad incorrecta de componentes, +Incorrect Invoice,Factura incorrecta, +Incorrect Movement Purpose,Movimiento incorrecto Propósito, +Incorrect Payment Type,Tipo de pago incorrecto, +Incorrect Reference Document (Purchase Receipt Item),Documento de referencia incorrecto (partida de recibo de compra), +Incorrect Serial No Valuation,Valoración incorrecta del número de serie, +Incorrect Serial Number Consumed,Número de serie incorrecto Consumido, +Incorrect Stock Value Report,Informe incorrecto sobre el valor de las existencias, +Incorrect Type of Transaction,Tipo de transacción incorrecto, +Increase In Asset Life(Months),Aumento de la vida útil del activo (meses), +Indent,Sangría, +Indirect Expense,Gastos Indirectos, +Individual GL Entry cannot be cancelled.,La inscripción GL individual no puede cancelarse., +Individual Stock Ledger Entry cannot be cancelled.,La entrada individual en el Libro Mayor no puede cancelarse., +Initialize Summary Table,Inicializar tabla resumen, +Insert New Records,Insertar nuevos registros, +Inspection Rejected,Inspección Rechazada, +Inspection Submission,Presentación de la inspección, +Instruction,Instrucción, +Insufficient Capacity,Capacidad Insuficiente, +Insufficient Stock for Batch,Stock insuficiente para el lote, +Inter Transfer Reference,Referencia de inter transferencia, +Interest and/or dunning fee,Intereses y/o gastos de reclamación, +Internal,Interno, +Internal Customer,Cliente Interno, +Internal Customer for company {0} already exists,Cliente Interno para empresa {0} ya existe, +Internal Sale or Delivery Reference missing.,Falta referencia de venta o entrega interna., +Internal Sales Reference Missing,Falta la referencia de ventas internas, +Internal Supplier,Proveedor Interno, +Internal Supplier for company {0} already exists,El Proveedor Interno de la compañía {0} ya existe, +Internal Transfer Reference Missing,Falta referencia de transferencia interna, +Internal Transfers,Transferencias Internas, +Internal transfers can only be done in company's default currency,Las transferencias internas solo se pueden realizar en la moneda predeterminada de la empresa., +Invalid,Inválido, +Invalid Allocated Amount,Importe asignado no válido, +Invalid Amount,Importe no válido, +Invalid Auto Repeat Date,Fecha de repetición automática inválida, +Invalid Cost Center,Centro de Costo Inválido, +Invalid Delivery Date,Fecha de Entrega Inválida, +Invalid Document,Documento inválido, +Invalid Document Type,Tipo de Documento Inválido, +Invalid Formula,Fórmula Inválida, +Invalid Group By,Agrupar por no válido, +Invalid Item Defaults,Artículos por defecto no válidos, +Invalid Ledger Entries,Entradas no válidas en el libro mayor, +Invalid Primary Role,Función principal no válida, +Invalid Priority,Prioridad inválida, +Invalid Process Loss Configuration,Configuración de pérdida de proceso no válida, +Invalid Purchase Invoice,Factura de Compra no válida, +Invalid Qty,Cant. inválida, +Invalid Schedule,Programación no válida, +Invalid Serial and Batch Bundle,Paquete de serie y lote no válidos, +Invalid Warehouse,Almacén inválido, +Invalid result key. Response:,Clave de resultado no válida. Respuesta:, +Invalid value {0} for {1} against account {2},Valor no válido {0} para {1} contra la cuenta {2}, +Inventory Dimension,Dimensión del inventario, +Inventory Dimension Negative Stock,Dimensión del inventario Existencias negativas, +Inventory Settings,Configuración de Inventario, +Invoice Cancellation,Cancelación de facturas, +Invoice Limit,Límite de facturación, +Invoice Portion (%),Porción de Factura (%), +Invoice and Billing,Factura y facturación, +Invoiced Qty,Cant. Facturada, +Invoices and Payments have been Fetched and Allocated,Se han obtenido y asignado facturas y pagos, +Invoicing Features,Características de Facturación, +Is Adjustment Entry,Es entrada de ajuste, +Is Alternative,Es Alternativo, +Is Cash or Non Trade Discount,Es Efectivo o Descuento no Comercial, +Is Composite Asset,Es Activo compuesto, +Is Corrective Job Card,Es Tarjeta de Trabajo Correctiva, +Is Corrective Operation,Es Operación Correctiva, +Is Expandable,Es expansible, +Is Finished Item,Es un artículo acabado, +Is Fully Depreciated,Está completamente Depreciado, +Is Group Warehouse,Es Almacén de Grupo, +Is Old Subcontracting Flow,Es un antiguo flujo de subcontratación, +Is Outward,Es hacia afuera, +Is Period Closing Voucher Entry,Es la entrada de comprobante de cierre de período, +Is Rate Adjustment Entry (Debit Note),Es Entrada de Ajuste de Tarifa (Nota de Débito), +Is Recursive,Es recursivo, +Is Rejected,Rechazado, +Is Rejected Warehouse,Es Almacén de Rechazados, +Is Scrap Item,Es artículo de desecho, +Is Short Year,Es Año corto, +Is Standard,Es estándar, +Is Stock Item,Es artículo de stock, +Is System Generated,Es generado por el sistema, +Is Tax Withholding Account,Es una cuenta de retención de impuestos, +Is Template,Es Plantilla, +Issue Analytics,Análisis de Incidencias, +Issue Summary,Resumen de Incidencias, +Issue a debit note with 0 qty against an existing Sales Invoice,Emitir una Nota de Débito con cantidad 0 contra una Factura de Venta existente, +Issuing cannot be done to a location. Please enter employee to issue the Asset {0} to,No se puede emitir a una ubicación. Ingrese el empleado al que se emitirá el activo {0}, +It can take upto few hours for accurate stock values to be visible after merging items.,Pueden pasar algunas horas hasta que los valores de stock precisos sean visibles después de fusionar los elementos., +"It's not possible to distribute charges equally when total amount is zero, please set 'Distribute Charges Based On' as 'Quantity'","No es posible distribuir los cargos equitativamente cuando el importe total es cero, por favor configure 'Distribuir cargos en base a' como 'Cantidad'.", +Item Code (Final Product),Código de artículo (producto final), +Item Group wise Discount,Descuento por grupo de artículos, +Item Price Settings,Configuración del precio del artículo, +"Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, Batch, UOM, Qty, and Dates.","El precio del producto aparece varias veces según la lista de precios, proveedor/cliente, moneda, producto, lote, unidad de medida, cantidad y fechas.", +Item Reference,Referencia del artículo, +Item Warehouse based reposting has been enabled.,Se ha habilitado el traspaso basado en el almacén de artículos., +Item and Warehouse,Producto y Almacén, +Item is removed since no serial / batch no selected.,El artículo se elimina al no haberse seleccionado ningún número de serie / lote., +Item qty can not be updated as raw materials are already processed.,La cantidad de artículos no puede actualizarse porque las materias primas ya están procesadas., +Item rate has been updated to zero as Allow Zero Valuation Rate is checked for item {0},La tasa del artículo se ha actualizado a cero ya que la opción Permitir tasa de valoración cero está marcada para el artículo {0}, +Item valuation reposting in progress. Report might show incorrect item valuation.,Traspaso de valoración de artículos en curso. El informe podría mostrar una valoración de artículos incorrecta., +Item {0} cannot be added as a sub-assembly of itself,El artículo {0} no puede añadirse como subconjunto de sí mismo, +Item {0} cannot be ordered more than {1} against Blanket Order {2}.,El artículo {0} no se puede pedir más de {1} contra el pedido general {2}., +Item {0} does not exist.,El artículo {0} no existe., +Item {0} entered multiple times.,Producto {0} ingresado varias veces., +Item {0} is already reserved/delivered against Sales Order {1}.,El artículo {0} ya está reservado/entregado contra el pedido de venta {1}., +Item {0} must be a Non-Stock Item,El artículo {0} debe ser un artículo que no se encuentra en stock, +Item {0} not found in 'Raw Materials Supplied' table in {1} {2},El artículo {0} no se encontró en la tabla 'Materias primas suministradas' en {1} {2}, +Item {0} not found.,Artículo {0} no encontrado., +Item {} does not exist.,Producto {0} no existe., +Items & Pricing,Productos y Precios, +Items Catalogue,Catálogo de Productos, +Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}.,Los artículos no se pueden actualizar ya que la orden de subcontratación se crea contra la orden de compra {0}., +Items rate has been updated to zero as Allow Zero Valuation Rate is checked for the following items: {0},"La tasa de artículos se ha actualizado a cero, ya que la opción Permitir tasa de valoración cero está marcada para los siguientes artículos: {0}", +Items to Be Repost,Artículos a reenviar, +Items to Order and Receive,Productos para Ordenar y Recibir, +Items to Reserve,Artículos para reservar, +Items {0} do not exist in the Item master.,Los artículos {0} no existen en el maestro de artículos., +Job Capacity,Capacidad de Trabajo, +Job Card Operation,Ficha de trabajo Operación, +Job Card Scheduled Time,Ficha de trabajo Hora programada, +Job Card Scrap Item,Ficha de trabajo Artículo de desecho, +Job Card and Capacity Planning,Tarjeta de trabajo y planificación de capacidad, +Job Cards,Tarjetas de Trabajo, +Job Paused,Trabajo en pausa, +Job Worker,Trabajador, +Job Worker Address,Dirección del trabajador, +Job Worker Address Details,Datos de la dirección del trabajador, +Job Worker Contact,Contacto del trabajador, +Job Worker Delivery Note,Albarán del trabajador, +Job Worker Name,Nombre del trabajador, +Job Worker Warehouse,Trabajador de almacén, +Job: {0} has been triggered for processing failed transactions,Trabajo: {0} se ha activado para procesar transacciones fallidas, +Joining,Uniéndose, +Journal Entries,Entradas de diario, +Journal Entry for Asset scrapping cannot be cancelled. Please restore the Asset.,No se puede cancelar la entrada del diario correspondiente al desguace de activos. Restaure el activo., +Journal Entry type should be set as Depreciation Entry for asset depreciation,El tipo de entrada de diario debe configurarse como Entrada de depreciación para la depreciación de activos., +Journal entries have been created,Se han creado entradas de diario, +Journals,Diarios, +Key,Clave, +Kindly cancel the Manufacturing Entries first against the work order {0}.,Por favor cancele primero las entradas de fabricación contra la orden de trabajo {0}., +"Last Name, Email or Phone/Mobile of the user are mandatory to continue.","Apellido, Email o Teléfono/Móvil del usuario son obligatorios para continuar.", +Lead -> Prospect,Cliente potencial -> Prospecto, +Lead Conversion Time,Tiempo de conversión de clientes potenciales, +Lead Owner cannot be same as the Lead Email Address,El propietario del cliente potencial no puede ser el mismo que la dirección de correo electrónico del cliente potencial, +Lead {0} has been added to prospect {1}.,El cliente potencial {0} se ha agregado al prospecto {1}., +Leaderboard,Tabla de líderes, +Leads,Clientes potenciales, +Learn Accounting,Aprender Contabilidad, +Learn Inventory Management,Aprende la Gestión del Inventario, +Learn Manufacturing,Aprender Manufactura, +Learn Procurement,Aprender Compras, +Learn Project Management,Aprender Gestión de Proyecto, +Learn Sales Management,Aprender Gestión de Ventas, +"Learn about Common Party","Conozca más sobre Partes comúnes", +"Leave blank for home. +This is relative to site URL, for example ""about"" will redirect to ""https://yoursitename.com/about""","Déjelo en blanco para la página de inicio. +Esto es relativo a la URL del sitio, por ejemplo ""acerca de"" redirigirá a ""https://yoursitename.com/about""", +Ledger Health,Estado del libro mayor, +Ledger Health Monitor,Monitor de estado del libro mayor, +Ledger Health Monitor Company,Monitor de estado del libro mayor Empresa, +Ledger Merge,Fusión de libro mayor, +Ledger Merge Accounts,Cuentas de fusión de libro mayor, +Ledgers,Libros mayores, +Left Child,Secundario izquierdo, +Legend,Leyenda, +Length,Largo, +Length (cm),Longitud (cm), +Less than 12 months.,Menos de 12 meses., +Level (BOM),Nivel (lista de materiales), +Limit timeslot for Stock Reposting,Limitar el tiempo disponible para la reubicación de existencias, +Limits don't apply on,Los límites no se aplican en, +Link a new bank account,Vincular una nueva cuenta bancaria, +Link with Customer,Enlace con el cliente, +Link with Supplier,Enlace con el proveedor, +Linked with submitted documents,Vinculado con los documentos validados, +Linking Failed,Enlace fallido, +Linking to Customer Failed. Please try again.,Error al vincular al cliente. Inténtalo de nuevo., +Linking to Supplier Failed. Please try again.,Error al vincular al proveedor. Inténtalo nuevamente., +Links,Enlaces, +Loading import file...,Cargando archivo de importación..., +Locked,Bloqueado, +Log Entries,Entradas de registro, +Log the selling and buying rate of an Item,Registra la tasa de venta y compra de un artículo, +Lost Quotations,Cotizaciones perdidas, +Lost Quotations %,Cotizaciones perdidas %, +Lost Reasons are required in case opportunity is Lost.,Se requieren motivos de pérdida en caso de que se pierda la oportunidad., +Lost Value,Valor perdido, +Lost Value %,% de valor perdido, +Machine Type,Tipo de Máquina, +Main Cost Center,Centro de Costo principal, +Main Cost Center {0} cannot be entered in the child table,El centro de costo principal {0} no se puede ingresar en la tabla secundaria, +Maintain Asset,Mantener activos, +Maintenance Details,Detalles de mantenimiento, +Make ,Crear , +Make Asset Movement,Realizar movimiento de activos, +Make Quotation,Hacer Cotización, +Make Return Entry,Hacer Entrada de Devolución, +Make Serial No / Batch from Work Order,Crear número de serie/lote a partir de la orden de trabajo, +Make {0} Variant,Hacer {0} variante, +Make {0} Variants,Hacer {0} variantes, +Making Journal Entries against advance accounts: {0} is not recommended. These Journals won't be available for Reconciliation.,No se recomienda realizar asientos contables contra cuentas anticipadas: {0} . Estos asientos contables no estarán disponibles para la conciliación., +Manage,Gestionar, +Mandatory Accounting Dimension,Dimensión contable obligatoria, +Mandatory Depends On,Obligatorio depende de, +Mandatory Field,Campo obligatorio, +Mandatory Section,Sección obligatoria, +Manual Inspection,Inspección Manual, +Manufacture Sub-assembly in Operation,Subensamblaje de Manufactura en Operación, +Manufacturing Type,Tipo de fabricación, +Mapping Purchase Receipt ...,Mapeando Recibo de Compra..., +Mapping Subcontracting Order ...,Mapeando órdenes de subcontratación..., +Mapping {0} ...,Mapeando {0} ..., +Margin View,Vista de Margen, +Mark As Closed,Marcar como cerrado, +Material Returned from WIP,Material devuelto de WIP, +Material Transfer (In Transit),Transferencia de material (en tránsito), +Materials are already received against the {0} {1},Los materiales ya se recibieron contra el {0} {1}, +Materials needs to be transferred to the work in progress warehouse for the job card {0},Es necesario transferir los materiales al almacén de trabajos en curso para la ficha de trabajo {0}, +Max Qty (As Per Stock UOM),Cantidad máxima (según unidad de medida en existencia), +Maximum Net Rate,Tasa Neta Máxima, +Maximum Payment Amount,Importe máximo del pago, +Maximum Value,Valor Máximo, +Maximum quantity scanned for item {0}.,Cantidad máxima escaneada para el artículo {0}., +Meeting,Reunión, +Mention if non-standard Receivable account,Indique si no es Cuenta por Cobrar estándar, +Merge Invoices Based On,Fusionar facturas en función de, +Merge Progress,Progreso de fusión, +Merge Similar Account Heads,Fusionar cuentas similares, +Merge taxes from multiple documents,Fusionar impuestos de varios documentos, +Merged,Combinado, +"Merging is only possible if following properties are same in both records. Is Group, Root Type, Company and Account Currency","La fusión solo es posible si las siguientes propiedades son las mismas en ambos registros: grupo, tipo de raíz, empresa y moneda de la cuenta.", +Merging {0} of {1},Fusionando {0} de {1}, +Min Qty (As Per Stock UOM),Cant. mín. (según UdM en almacén), +Min Qty should be greater than Recurse Over Qty,La cantidad mínima debe ser mayor que la cantidad recursiva, +Minimum Net Rate,Tasa Neta Mínima, +Minimum Payment Amount,Importe mínimo de pago, +Minimum Value,Valor mínimo, +Mismatch,Desajuste, +Missing,Faltante, +Missing Asset,Activo faltante, +Missing Cost Center,Centro de costos faltante, +Missing Finance Book,Falta el libro de finanzas, +Missing Finished Good,Falta Acabado Bueno, +Missing Formula,Falta fórmula, +Missing Items,Faltan artículos, +Missing Payments App,Aplicación de pagos faltantes, +Missing Serial No Bundle,Falta el número de serie del paquete, +Missing value,Valor faltante, +Modified By,Modificado por, +Modified On,Modificado el, +Module Settings,Configuración de Módulos, +Monitor for Last 'X' days,"Monitorización de los últimos ""X"" días", +Move Stock,Mover Stock, +Move to Cart,Mover al carrito, +Movement,Movimiento, +Moving up in tree ...,Subiendo en árbol ..., +Multi-level BOM Creator,Creador de listas de materiales multinivel, +Multiple Loyalty Programs found for Customer {}. Please select manually.,Se encontraron varios programas de fidelización para el cliente {}. Seleccione manualmente., +Multiple Warehouse Accounts,Múltiples cuentas de almacén, +Multiple items cannot be marked as finished item,No se pueden marcar varios artículos como artículo terminado, +Must be a publicly accessible Google Sheets URL and adding Bank Account column is necessary for importing via Google Sheets,Debe ser una URL de Hojas de cálculo de Google de acceso público y es necesario agregar la columna Cuenta bancaria para importar a través de Hojas de cálculo de Google., +Named Place,Lugar nombrado, +Naming Series and Price Defaults,Series de Nombres y Precios por Defecto, +Net total calculation precision loss,Pérdida neta total de precisión de cálculo, +New Balance In Account Currency,Nuevo saldo en la moneda de la cuenta, +New Event,Nuevo Evento, +New Note,Nueva Nota, +New Task,Nueva Tarea, +New Version,Nueva versión, +Newsletter,Boletín de noticias, +No Answer,Sin respuesta, +No Customers found with selected options.,No se encontraron clientes con las opciones seleccionadas., +No Items selected for transfer.,No hay artículos seleccionados para transferir., +No Matching Bank Transactions Found,No se han encontrado transacciones bancarias coincidentes, +No Notes,Sin notas, +No Outstanding Invoices found for this party,No se encontraron facturas pendientes para esta parte, +No POS Profile found. Please create a New POS Profile first,"No se ha encontrado ningún Perfil de PV. Por favor, cree primero un Nuevo Perfil PV", +No Records for these settings.,No hay registros para estas configuraciones., +No Serial / Batches are available for return,No hay números de serie ni lotes disponibles para devolución, +No Stock Available Currently,No hay existencias disponibles actualmente, +No Summary,Sin resumen, +No Tax Withholding data found for the current posting date.,No se han encontrado datos de retenciones fiscales para la fecha de contabilización actual., +No Terms,Sin términos, +No Unreconciled Invoices and Payments found for this party and account,No se encontraron facturas ni pagos sin conciliar para esta parte y cuenta, +No Unreconciled Payments found for this party,No se encontraron pagos no conciliados para esta parte, +No Work Orders were created,No se crearon órdenes de trabajo, +No additional fields available,No hay campos adicionales disponibles, +No billing email found for customer: {0},No se encontró ningún correo electrónico de facturación para el cliente: {0}, +No data found. Seems like you uploaded a blank file,No se encontraron datos. Parece que has subido un archivo en blanco., +No employee was scheduled for call popup,No se programó ningún empleado para la ventana emergente de llamada, +No failed logs,No hay registros fallidos, +No item available for transfer.,No hay ningún artículo disponible para transferencia., +No items are available in sales orders {0} for production,No hay artículos disponibles en los pedidos de venta {0} para producción, +No items are available in the sales order {0} for production,No hay artículos disponibles en la orden de venta {0} para producción, +No items in cart,No hay artículos en el carrito, +No matches occurred via auto reconciliation,No se produjeron coincidencias mediante la conciliación automática, +No more children on Left,No más secundarios en la izquierda, +No more children on Right,No más secundarios en la derecha, +No of Docs,Nº de documentos, +No of Employees,Núm. de Empleados, +No of Months (Expense),Nº de meses (gastos), +No of Months (Revenue),Nº de meses (Ingresos), +No open event,Ningún evento abierto, +No open task,Sin tareas abiertas, +No outstanding {0} found for the {1} {2} which qualify the filters you have specified.,No se encontraron {0} pendientes para los {1} {2} que califican para los filtros que ha especificado., +No primary email found for customer: {0},No se encontró ningún correo electrónico principal para el cliente: {0}, +No records found in Allocation table,No se encontraron registros en la tabla de asignación, +No records found in the Invoices table,No se encontraron registros en la tabla Facturas, +No records found in the Payments table,No se encontraron registros en la tabla Pagos, +No stock transactions can be created or modified before this date.,No se podrán crear ni modificar transacciones de stock antes de esta fecha., +No {0} Accounts found for this company.,No se han encontrado cuentas en {0} para esta empresa., +No.,Nº, +No. of Employees,Núm. de Empleados, +No. of parallel job cards which can be allowed on this workstation. Example: 2 would mean this workstation can process production for two Work Orders at a time.,Nº de tarjetas de trabajo paralelas que se pueden permitir en esta estación de trabajo. Ejemplo: 2 significaría que esta estación de trabajo puede procesar la producción de dos Órdenes de Trabajo a la vez., +Note: Automatic log deletion only applies to logs of type Update Cost,Nota: El borrado automático de registros sólo se aplica a los registros de tipo Coste de actualización, +"Note: To merge the items, create a separate Stock Reconciliation for the old item {0}","Nota: Para fusionar los artículos, cree una reconciliación de existencias separada para el antiguo artículo {0}.", +Notes HTML,Notas HTML, +Notification,Notificación, +Notification Settings,Configuración de las notificaciones, +Notify Reposting Error to Role,Notificar error de reenvío al rol, +Number of Days,Número de días, +Numeric,Numérico, +Numeric Inspection,Inspección numérica, +Off,Apagado, +Offsetting Account,Cuenta de compensación, +Offsetting for Accounting Dimension,Compensación de la dimensión contable, +On Paid Amount,Sobre el importe pagado, +On This Date,En esta fecha, +On Track,En marcha, +On enabling this cancellation entries will be posted on the actual cancellation date and reports will consider cancelled entries as well,Al habilitar esta cancelación las entradas se contabilizarán en la fecha real de cancelación y los informes también tendrán en cuenta las entradas canceladas, +"On expanding a row in the Items to Manufacture table, you'll see an option to 'Include Exploded Items'. Ticking this includes raw materials of the sub-assembly items in the production process.","Al expandir una fila en la tabla de Artículos para fabricar, verá una opción para ""Incluir artículos despiezados"". Al marcar esta opción, se incluyen las materias primas de los artículos del subconjunto en el proceso de producción.", +"On submission of the stock transaction, system will auto create the Serial and Batch Bundle based on the Serial No / Batch fields.","En la validación la transacción de existencias, el sistema creará automáticamente el lote de series y lotes basándose en los campos Número de serie / Lote.", +Once the Work Order is Closed. It can't be resumed.,Una vez cerrada la Orden de Trabajo. No se puede reanudar., +Only 'Payment Entries' made against this advance account are supported.,Sólo se admiten 'Entradas de pago' realizadas contra esta cuenta de anticipo., +Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload,Solo se pueden utilizar archivos CSV y Excel para importar datos. Verifique el formato del archivo que intenta cargar, +Only Deduct Tax On Excess Amount ,Deducir impuestos solo sobre el importe excedente , +Only Include Allocated Payments,Incluir sólo los pagos asignados, +Only Parent can be of type {0},Sólo el principal puede ser del tipo {0}, +Only existing assets,Solo activos existentes, +"Only one Subcontracting Order can be created against a Purchase Order, cancel the existing Subcontracting Order to create a new one.",Solo se puede crear una orden de subcontratación contra una orden de compra; cancele la orden de subcontratación existente para crear una nueva., +Only one {0} entry can be created against the Work Order {1},Solo se puede crear una entrada {0} contra la orden de trabajo {1}, +"Only values between [0,1) are allowed. Like {0.00, 0.04, 0.09, ...} +Ex: If allowance is set at 0.07, accounts that have balance of 0.07 in either of the currencies will be considered as zero balance account","Solo se permiten valores entre [0,1). Como {0,00, 0,04, 0,09, ...} +Ejemplo: si la asignación se establece en 0,07, las cuentas que tengan un saldo de 0,07 en cualquiera de las monedas se considerarán cuentas con saldo cero.", +Only {0} are supported,Solo se admiten {0}, +Open Activities HTML,Abrir actividades HTML, +Open Call Log,Abrir registro de llamadas, +Open Event,Abrir Evento, +Open Events,Eventos abiertos, +Open Sales Orders,Órdenes de venta abiertas, +Open Task,Abrir tarea, +Open Tasks,Tareas abiertas, +Open Work Order {0},Orden de trabajo abierta {0}, +Opening & Closing,Apertura y cierre, +Opening Accumulated Depreciation must be less than or equal to {0},La depreciación acumulada de apertura debe ser inferior o igual a {0}., +Opening Entry can not be created after Period Closing Voucher is created.,El asiento de apertura no puede crearse después de haber creado el comprobante de cierre del período., +Opening Number of Booked Depreciations,Número de apertura de depreciaciones registradas, +Opening Purchase Invoices have been created.,Se han creado facturas de compra de apertura., +Opening Sales Invoices have been created.,Se han creado facturas de venta de apertura., +Operating Cost Per BOM Quantity,Coste operativo por cantidad de la lista de materiales, +Operation time does not depend on quantity to produce,El tiempo de operación no depende de la cantidad a producir, +Opportunity Amount (Company Currency),Valor de la oportunidad (moneda de la empresa), +Opportunity Owner,Propietario de oportunidad, +Opportunity Source,Fuente de oportunidad, +Opportunity Summary by Sales Stage,Resumen de oportunidades por etapa de venta, +Opportunity Summary by Sales Stage ,Resumen de oportunidades por etapa de venta , +Opportunity Value,Valor de oportunidad, +Order Status,Estado del Pedido, +Other Info,Información adicional, +Out of stock,Agotado, +Over Picking Allowance,Tolerancia por exceso de recogida, +Over Receipt,Sobre recibo, +Over Receipt/Delivery of {0} {1} ignored for item {2} because you have {3} role.,Se ignora la recepción/entrega excesiva de {0} {1} para el artículo {2} porque tiene el rol {3} ., +Over Transfer Allowance,Exceso de asignación por transferencia, +Overbilling of {0} {1} ignored for item {2} because you have {3} role.,Sobrefacturación de {0} {1} ignorada para el artículo {2} porque tiene el rol {3} ., +Overbilling of {} ignored because you have {} role.,Se ignora la sobrefacturación de {} porque tiene el rol {}., +Overdue Payment,Pago vencido, +Overdue Payments,Pagos vencidos, +Overdue Tasks,Tareas atrasadas, +Overview,Visión general, +PDF Name,Nombre del PDF, +POS Closing Failed,Cierre de TPV fallido, +POS Closing failed while running in a background process. You can resolve the {0} and retry the process again.,El Cierre del TPV falló mientras se ejecutaba un proceso en segundo plano. Puede resolver el {0} y reintentar el proceso de nuevo., +POS Invoice is already consolidated,La factura del TPV ya está consolidada, +POS Invoice is not submitted,La factura del TPV no se presenta, +POS Invoice should have the field {0} checked.,La factura del TPV debe tener el campo {} marcado., +POS Invoices will be consolidated in a background process,Las facturas de TPV se consolidarán en un proceso en segundo plano, +POS Invoices will be unconsolidated in a background process,Las facturas de TPV no se consolidarán en un proceso en segundo plano, +POS Profile doesn't match {},El perfil de TPV no coincide con {}, +POS Profile {} contains Mode of Payment {}. Please remove them to disable this mode.,"El Perfil de TPV {} contiene el Modo de Pago {}. Por favor, elimínelos para desactivar este modo.", +POS Search Fields,Campos de búsqueda en el TPV, +POS Setting,Ajuste del TPV, +Package No(s) already in use. Try from Package No {0},Los números de paquete ya están en uso. Pruebe desde el número de paquete {0}, +Packaging Slip From Delivery Note,Recibo de embalaje de la nota de entrega, +Packed Items cannot be transferred internally,Los artículos empaquetados no se pueden transferir internamente, +Packed Qty,Cantidad empaquetada, +Page Break After Each SoA,Salto de página después de cada SoA, +Paid Amount After Tax,Importe pagado después de impuestos, +Paid Amount After Tax (Company Currency),Importe pagado después de impuestos (moneda de la empresa), +Paid From Account Type,Pagado desde tipo de cuenta, +Paid To Account Type,Tipo de cuenta pagada, +Pallets,Palés, +Parameter Group,Grupo de parámetros, +Parameter Group Name,Nombre del grupo de parámetros, +Parcel Template,Plantilla de paquete, +Parcel Template Name,Nombre de la plantilla de parcela, +Parcel weight cannot be 0,El peso del paquete no puede ser 0, +Parcels,Paquetes, +Parent Account Missing,Falta la cuenta principal, +Parent Document,Documento Principal, +Parent Item {0} must not be a Fixed Asset,El artículo principal {0} no debe ser un activo fijo, +Parent Row No,Número de fila principal, +Parent Task {0} is not a Template Task,La tarea principal {0} no es una tarea de plantilla, +Partial Material Transferred,Material parcial transferido, +Partial Stock Reservation,Reserva parcial de stock, +Partial Success,Éxito Parcial, +"Partial stock can be reserved. For example, If you have a Sales Order of 100 units and the Available Stock is 90 units then a Stock Reservation Entry will be created for 90 units. ","Se puede reservar un stock parcial. Por ejemplo, si tiene un pedido de venta de 100 unidades y el stock disponible es de 90 unidades, se creará una entrada de reserva de stock para 90 unidades. ", +Partially Delivered,Entregado parcialmente, +Partially Reconciled,Parcialmente reconciliado, +Partially Reserved,Parcialmente reservado, +Partly Paid,Pagado parcialmente, +Partly Paid and Discounted,Parcialmente pagado y descontado, +Partnership,Asociación, +Party Account No. (Bank Statement),Número de cuenta de la Entidad(extracto bancario), +Party Account {0} currency ({1}) and document currency ({2}) should be same,La moneda de la cuenta de la Entidad {0} ({1}) y la moneda del documento ({2}) deben ser las mismas, +Party IBAN (Bank Statement),IBAN del destinatario (extracto bancario), +Party Item Code,Código de artículo de la Entidad, +Party Link,Enlace de la Entidad, +Party Name/Account Holder (Bank Statement),Nombre de la Entidad/titular de la cuenta (extracto bancario), +Party Specific Item,Producto específico de la Parte, +Party Type and Party is required for Receivable / Payable account {0},Se requiere el tipo de Entidad y la Entidad para la cuenta por cobrar/pagar {0}, +Party can only be one of {0},La Entidad solo puede ser una de {0}, +Passport Details,Datos del pasaporte, +Pause Job,Pausar trabajo, +Paused,Pausado, +Payment Amount (Company Currency),Importe del pago (moneda de la empresa), +"Payment Entry {0} is linked against Order {1}, check if it should be pulled as advance in this invoice.","La entrada de pago {0} está vinculada al pedido {1}, verifique si debe extraerse como anticipo en esta factura.", +Payment Ledger,Libro de pagos, +Payment Ledger Balance,Saldo del libro mayor de pagos, +Payment Ledger Entry,Entrada de libro mayor de pagos, +Payment Limit,Límite de pago, +Payment Reconciliation Allocation,Asignación de conciliación de pagos, +Payment Reconciliation Job: {0} is running for this party. Can't reconcile now.,Trabajo de conciliación de pagos: {0} se está ejecutando para esta parte. No se puede conciliar ahora., +Payment Reconciliations,Conciliaciones de pagos, +Payment Request Outstanding,Solicitud de pago pendiente, +Payment Request created from Sales Order or Purchase Order will be in Draft status. When disabled document will be in unsaved state.,"La solicitud de pago creada a partir de una orden de venta o una orden de compra estará en estado de borrador. Si está deshabilitada, el documento estará en estado de no guardado.", +Payment Request is already created,La solicitud de pago ya está creada, +Payment Request took too long to respond. Please try requesting for payment again.,La solicitud de pago tardó demasiado en responder. Intente solicitar el pago nuevamente., +Payment Requests cannot be created against: {0},No se pueden crear solicitudes de pago contra: {0}, +Payment Term Outstanding,Plazo de pago pendiente, +Payment Terms Status for Sales Order,Estado de las condiciones de pago de la orden de venta, +Payment Terms from orders will be fetched into the invoices as is,Las condiciones de pago de los pedidos se reflejarán en las facturas tal como están., +Payment Unlink Error,Error al desvincular el pago, +Payment of {0} received successfully.,Pago de {0} recibido exitosamente., +Payment of {0} received successfully. Waiting for other requests to complete...,Pago de {0} recibido con éxito. Esperando que se completen otras solicitudes..., +Payment request failed,Solicitud de pago fallida, +Payment term {0} not used in {1},Término de pago {0} no utilizado en {1}, +Pending processing,Pendiente de procesamiento, +Per Received,Por recibido, +Percentage (%),Porcentaje (%), +Percentage you are allowed to order beyond the Blanket Order quantity.,Porcentaje que se le permite pedir más allá de la cantidad del pedido general., +Percentage you are allowed to sell beyond the Blanket Order quantity.,Porcentaje que se le permite vender más allá de la cantidad del pedido general., +Period Closed,Período cerrado, +Period Closing Entry For Current Period,Asiento de cierre de período para el período actual, +Period Closing Settings,Configuración de cierre de período, +Period Details,Detalles del periodo, +Period To Date,Período hasta la fecha, +Period_from_date,Periodo_desde_fecha, +Phone Ext.,Extensión telefónica, +Pick List Incomplete,Lista de selección incompleta, +Pick Manually,Seleccionar manualmente, +Pick Serial / Batch Based On,Selección de serie / lote basada en, +Pick Serial / Batch No,Seleccione el número de serie/lote, +Picked Qty (in Stock UOM),Cantidad seleccionada (en stock UdM), +Pickup,Recogida, +Pickup Contact Person,Persona de contacto para la recogida, +Pickup Date,Fecha de recogida, +Pickup Date cannot be before this day,La fecha de recogida no puede ser anterior a este día., +Pickup From,Recoger de, +Pickup To time should be greater than Pickup From time,La hora de recogida hasta debe ser mayor que la hora de recogida desde, +Pickup Type,Tipo de recogida, +Pickup from,Recoger de, +Pickup to,Recogida a, +Pipeline By,Fuente de información por, +Plaid Link Failed,Vinculación a Plaid fallido, +Plaid Link Refresh Required,Se requiere actualización de la vinculación con Plaid, +Plaid Link Updated,Enlace de Plaid actualizado, +Plan to Request Qty,Plan para solicitar cantidad, +Plant Dashboard,Panel de control de la planta, +Plant Floor,Planta, +Please Set Priority,"Por favor, establezca la prioridad", +Please Specify Account,Por favor especifique la cuenta, +Please add 'Supplier' role to user {0}.,"Por favor, añada el rol 'Proveedor' al usuario {0}.", +Please add Request for Quotation to the sidebar in Portal Settings.,"Por favor, añada la Solicitud de Presupuesto a la barra lateral en los Ajustes del Portal.", +Please add Root Account for - {0},"Por favor, añada una cuenta raíz para - {0}", +Please add atleast one Serial No / Batch No,"Por favor, añada al menos un nº de serie / nº de lote", +Please add the Bank Account column,"Por favor, añada la columna Cuenta bancaria", +Please add the account to root level Company - {0},"Por favor, añada la cuenta al nivel raíz Empresa - {0}", +Please add {1} role to user {0}.,"Por favor, añada el rol {1} al usuario {0}.", +Please adjust the qty or edit {0} to proceed.,Ajuste la cantidad o edite {0} para continuar., +Please attach CSV file,Adjunte el archivo CSV, +Please cancel and amend the Payment Entry,"Por favor, cancele y modifique la Entrada de Pago", +Please cancel payment entry manually first,"Por favor, cancele primero la entrada del pago manualmente", +Please cancel related transaction.,"Por favor, cancele la transacción relacionada.", +Please check Process Deferred Accounting {0} and submit manually after resolving errors.,"Por favor, marque Procesar contabilidad diferida {0} y valídelo manualmente después de resolver los errores.", +Please check either with operations or FG Based Operating Cost.,Consulte con operaciones o con el costo operativo basado en FG., +Please check the error message and take necessary actions to fix the error and then restart the reposting again.,"Por favor, compruebe el mensaje de error y tome las medidas necesarias para solucionar el error y luego reinicie el reenvío de nuevo.", +Please check your email to confirm the appointment,"Por favor, compruebe su correo electrónico para confirmar la cita", +Please contact any of the following users to extend the credit limits for {0}: {1},Comuníquese con cualquiera de los siguientes usuarios para ampliar los límites de crédito para {0}: {1}, +Please contact any of the following users to {} this transaction.,"Por favor, póngase en contacto con cualquiera de los siguientes usuarios para {} esta transacción.", +Please contact your administrator to extend the credit limits for {0}.,Póngase en contacto con su administrador para ampliar los límites de crédito de {0}., +Please create Landed Cost Vouchers against Invoices that have 'Update Stock' enabled.,"Cree comprobantes de costo de destino contra facturas que tengan habilitada la opción ""Actualizar stock"".", +Please create a new Accounting Dimension if required.,"Por favor, cree una nueva Dimensión Contable si es necesario.", +Please create purchase from internal sale or delivery document itself,"Por favor, cree la compra a partir de la venta interna o del propio documento de entrega", +"Please delete Product Bundle {0}, before merging {1} into {2}","Por favor, elimine el paquete de productos {0}, antes de fusionar {1} en {2}", +Please do not book expense of multiple assets against one single Asset.,"Por favor, no contabilice gastos de múltiples activos contra un único Activo.", +Please enable Use Old Serial / Batch Fields to make_bundle,"Por favor, active Usar campos de serie / lote antiguos en make_bundle", +Please enable only if the understand the effects of enabling this.,Habilítelo solo si comprende los efectos de habilitar esto., +Please enable {0} in the {1}.,"Por favor, habilite {0} en la página {1}.", +Please enable {} in {} to allow same item in multiple rows,"Por favor, active {} en {} para permitir el mismo elemento en varias filas", +Please ensure that the {0} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account.,Asegúrese de que la cuenta {0} es una cuenta de Balance. Puede cambiar la cuenta padre a una cuenta de Balance o seleccionar una cuenta diferente., +Please ensure that the {0} account {1} is a Payable account. You can change the account type to Payable or select a different account.,Asegúrese de que la cuenta {0} {1} sea una cuenta de pago. Puede cambiar el tipo de cuenta a pago o seleccionar una cuenta diferente., +Please ensure {} account is a Balance Sheet account.,Asegúrese de que la cuenta {} sea una cuenta de balance general., +Please ensure {} account {} is a Receivable account.,Asegúrese de que {} cuenta {} sea una cuenta por cobrar., +Please enter Root Type for account- {0},"Por favor, introduzca el tipo de cuenta- {0}", +Please enter Serial Nos,"Por favor, introduzca Serial Nos", +Please enter Shipment Parcel information,"Por favor, introduzca la información del paquete de envío", +Please enter Stock Items consumed during the Repair.,Ingrese los artículos en stock consumidos durante la reparación., +Please enter mobile number first.,"Por favor, introduzca primero el número de móvil.", +Please enter quantity for item {0},Ingrese la cantidad del artículo {0}, +Please enter serial nos,"Por favor, introduzca los números de serie", +"Please first set Last Name, Email and Phone for the user","Primero configure el apellido, el correo electrónico y el teléfono del usuario.", +Please fix overlapping time slots for {0},"Por favor, arreglen los espacios de tiempo superpuestos para {0}", +Please fix overlapping time slots for {0}.,"Por favor, arregle los espacios de tiempo superpuestos para {0}.", +Please import accounts against parent company or enable {} in company master.,"Por favor, importe las cuentas contra la empresa principal o habilite {} en el maestro de empresas.", +"Please keep one Applicable Charges, when 'Distribute Charges Based On' is 'Distribute Manually'. For more charges, please create another Landed Cost Voucher.","Conserve un solo Cargo aplicable cuando la opción ""Distribuir cargos según"" esté configurada como ""Distribuir manualmente"". Para más cargos, cree otro Comprobante de costo de destino.", +Please make sure the file you are using has 'Parent Account' column present in the header.,Asegúrese de que el archivo que está utilizando tenga la columna 'Cuenta principal' presente en el encabezado., +Please mention 'Weight UOM' along with Weight.,Mencione 'Peso UdM' junto con el Peso., +Please mention the Current and New BOM for replacement.,"Por favor, mencione la lista de materiales actual y la nueva para la sustitución.", +Please rectify and try again.,"Por favor, corrija y vuelva a intentarlo.", +Please refresh or reset the Plaid linking of the Bank {}.,"Por favor, actualice o reinicie la vinculación del Banco con Plaid {}.", +Please save before proceeding.,"Por favor, guarde antes de continuar.", +Please select Bank Account,"Por favor, seleccione Cuenta Bancaria", +Please select Finished Good Item for Service Item {0},"Por favor, seleccione Artículo de producto terminado para el artículo de servicio {0}", +Please select Serial/Batch Nos to reserve or change Reservation Based On to Qty.,Seleccione los números de serie/lote para reservar o cambie Reserva basada en a Cantidad., +Please select Subcontracting Order instead of Purchase Order {0},Por favor seleccione Orden de Subcontratación en lugar de Orden de Compra {0}, +Please select Unrealized Profit / Loss account or add default Unrealized Profit / Loss account account for company {0},Seleccione la cuenta de ganancias/pérdidas no realizadas o agregue la cuenta de ganancias/pérdidas no realizadas predeterminada para la empresa {0}, +Please select a Subcontracting Purchase Order.,Seleccione una orden de compra de subcontratación., +Please select a Warehouse,Por favor seleccione un almacén, +Please select a Work Order first.,Seleccione primero una orden de trabajo., +Please select a country,"Por favor, selecciona un país", +Please select a customer for fetching payments.,Seleccione un cliente para obtener los pagos., +Please select a date,"Por favor, seleccione una fecha", +Please select a date and time,Por favor seleccione una fecha y hora, +Please select a row to create a Reposting Entry,Seleccione una fila para crear una entrada de reenvío, +Please select a supplier for fetching payments.,"Por favor, seleccione un proveedor para obtener los pagos.", +Please select a valid Purchase Order that has Service Items.,"Por favor, seleccione una Orden de Compra válida que tenga Artículos de Servicio.", +Please select a valid Purchase Order that is configured for Subcontracting.,"Por favor, seleccione un Pedido válido que esté configurado para Subcontratación.", +Please select an item code before setting the warehouse.,Seleccione un código de artículo antes de configurar el almacén., +Please select either the Item or Warehouse or Warehouse Type filter to generate the report.,Seleccione el filtro Artículo o Almacén o Tipo de almacén para generar el informe., +Please select items,Por favor seleccione artículos, +Please select items to reserve.,Por favor seleccione los artículos que desea reservar., +Please select items to unreserve.,Seleccione los artículos que desea cancelar la reserva., +Please select only one row to create a Reposting Entry,Seleccione solo una fila para crear una entrada de reenvío, +Please select rows to create Reposting Entries,Seleccione filas para crear entradas de reenvío, +Please select the required filters,Por favor seleccione los filtros requeridos, +Please select valid document type.,Seleccione un tipo de documento válido., +Please set Account,"Por favor, establezca una cuenta", +Please set Accounting Dimension {} in {},"Por favor, establezca la dimensión contable {} en {}", +Please set Email/Phone for the contact,"Por favor, establezca Email/Teléfono para el contacto", +Please set Fiscal Code for the customer '%s',"Por favor, establezca el código fiscal para el cliente '%s'", +Please set Fiscal Code for the public administration '%s',"Por favor, establezca el código fiscal para la administración pública '%s'", +Please set Fixed Asset Account in {} against {}.,Establezca la cuenta de activos fijos en {} frente a {}., +Please set Opening Number of Booked Depreciations,"Por favor, establezca el número de apertura de las depreciaciones registradas", +Please set Parent Row No for item {0},Establezca el número de fila principal para el artículo {0}, +Please set Root Type,"Por favor, configure el tipo de raíz", +Please set Tax ID for the customer '%s',"Por favor, establezca el número de identificación fiscal para el cliente '%s'", +Please set VAT Accounts in {0},Establezca las cuentas de IVA en {0}, +"Please set Vat Accounts for Company: ""{0}"" in UAE VAT Settings","Establezca las cuentas de IVA para la empresa: ""{0}"" en la configuración de IVA de los EAU", +Please set a Cost Center for the Asset or set an Asset Depreciation Cost Center for the Company {},Establezca un centro de costo para el activo o establezca un centro de costo de depreciación de activos para la empresa {}, +Please set a default Holiday List for Company {0},Establezca una lista de días festivos predeterminada para la empresa {0}, +Please set an Address on the Company '%s',"Por favor, establezca una dirección en la empresa '%s'", +Please set an Expense Account in the Items table,Establezca una cuenta de gastos en la tabla de artículos, +Please set default Exchange Gain/Loss Account in Company {},Establezca la cuenta de ganancias/pérdidas de cambio predeterminada en la empresa {}, +Please set default Expense Account in Company {0},Establezca la cuenta de gastos predeterminada en la empresa {0}, +Please set default cost of goods sold account in company {0} for booking rounding gain and loss during stock transfer,Establezca la cuenta de costo de bienes vendidos predeterminada en la empresa {0} para registrar las ganancias y pérdidas por redondeo durante la transferencia de existencias, +Please set either the Tax ID or Fiscal Code on Company '%s',Establezca el ID fiscal o el código fiscal en la empresa '%s', +Please set filters,"Por favor, defina los filtros", +Please set one of the following:,Establezca una de las siguientes opciones:, +Please set the cost center field in {0} or setup a default Cost Center for the Company.,Establezca el campo del centro de costo en {0} o configure un centro de costo predeterminado para la empresa., +Please set {0} in BOM Creator {1},Establezca {0} en LdM Creator {1}, +Please setup and enable a group account with the Account Type - {0} for the company {1},"Por favor, configura y habilita una cuenta de grupo con el tipo de cuenta - {0} para la empresa {1}.", +Please share this email with your support team so that they can find and fix the issue.,Comparta este correo electrónico con su equipo de soporte para que puedan encontrar y solucionar el problema., +Please specify a {0},Por favor especifique un {0}, +Please try again in an hour.,Vuelve a intentarlo en 1 hora., +Please update Repair Status.,"Por favor, actualice el estado de la reparación.", +Portal User,Usuario del portal, +Portal Users,Usuario del Portal, +Posting Datetime,Fecha y hora de publicación, +Powered by {0},Desarrollado por {0}, +"Previous Year is not closed, please close it first","El año anterior no está cerrado, por favor ciérrelo primero", +Price ({0}),Precio ({0}), +Price List Defaults,Lista de precios por defecto, +Price Per Unit ({0}),Precio por Unidad ({0}), +Primary Address and Contact,Dirección principal y Contacto, +Primary Contact,Contacto Principal, +Primary Party,Entidad Primaria, +Primary Role,Rol principal, +Print Format Builder,Diseñador de formatos de impresión, +Print Style,Estilo de Impresión, +Printing,Impresión, +Priority cannot be lesser than 1.,La prioridad no puede ser menor a 1., +Priority is mandatory,La prioridad es obligatoria, +Probability,Probabilidad, +Process Loss,Pérdida por Proceso, +Process Loss Percentage cannot be greater than 100,El porcentaje de pérdida de proceso no puede ser mayor que 100, +Process Loss Qty,Cantidad de pérdida de proceso, +Process Loss Report,Informe de pérdidas de proceso, +Process Loss Value,Valor de pérdida del proceso, +Process Payment Reconciliation,Proceso de conciliación de pagos, +Process Payment Reconciliation Log,Registro de conciliación de procesos de pago, +Process Payment Reconciliation Log Allocations,Asignaciones del registro de conciliación de pagos del proceso, +Process Subscription,Proceso de suscripción, +Process in Single Transaction,Proceso en Transacción Única, +Processed BOMs,Listas de materiales procesadas, +Processing Sales! Please Wait...,¡Procesando ventas! Por favor espere..., +Produced / Received Qty,Cantidad producida/recibida, +Product Price ID,ID del Precio del producto, +Production Plan Already Submitted,Plan de producción ya validado, +Production Plan Item Reference,Plan de producción Articulo de referencia, +Production Plan Qty,Plan de producción Cantidad, +Production Plan Sub Assembly Item,Plan de producción Elemento de subensamblaje, +Production Plan Sub-assembly Item,Plan de producción Subconjunto Artículo, +Production Plan Summary,Resumen del plan de producción, +Profile,Perfil, +Profit and Loss Summary,Resumen de pérdidas y ganancias, +Progress,Progreso, +Progress (%),Progreso (%), +Project Progress:,Progreso del proyecto:, +Prompt Qty,Cantidad inmediata, +Prospect,Prospecto, +Prospect Lead,Prospecto de cliente potencial, +Prospect Opportunity,Oportunidad prospecto, +Prospect Owner,Propietario del prospecto, +Prospect {0} already exists,El prospecto {0} ya existe, +Provisional Account,Cuenta provisional, +Provisional Expense Account,Cuenta de Gastos Provisionales, +Purchase Order Item reference is missing in Subcontracting Receipt {0},Falta la referencia del artículo de la orden de compra en el recibo de subcontratación {0}, +Purchase Orders {0} are un-linked,Las órdenes de compra {0} no están vinculadas, +Purchase Receipt (Draft) will be auto-created on submission of Subcontracting Receipt.,El recibo de compra (borrador) se creará automáticamente al validar el recibo de subcontratación., +Purchase Receipt {0} created.,Recibo de compra {0} creado., +Purchase Value,Valor de compra, +Purchases,Compras, +Purposes Required,Propósitos requeridos, +Putaway Rule,Regla de almacenamiento, +Putaway Rule already exists for Item {0} in Warehouse {1}.,La regla de almacenamiento ya existe para el artículo {0} en el almacén {1}., +Qty ,Cant. , +Qty After Transaction,Cantidad después de la transacción, +Qty As Per BOM,Cantidad según lista de materiales, +Qty Change,Cantidad Cambio, +Qty In Stock,Cant en existencia, +Qty Per Unit,Cantidad por unidad, +"Qty To Manufacture ({0}) cannot be a fraction for the UOM {2}. To allow this, disable '{1}' in the UOM {2}.","La cantidad a fabricar ({0}) no puede ser una fracción para la unidad de medida {2}. Para permitir esto, deshabilite '{1}' en la unidad de medida {2}.", +Qty To Produce,Cantidad a producir, +Qty and Rate,Cantidad y Tarifa, +Qty as Per Stock UOM,Cantidad según stock UdM, +Qty for which recursion isn't applicable.,Cantidad para la que no es aplicable la recursividad., +Qty in Stock UOM,Cantidad en stock UdM, +Qty of Finished Goods Item should be greater than 0.,La cantidad de productos acabados debe ser superior a 0., +Qty to Be Consumed,Cantidad para ser consumida, +Qty to Build,Cantidad a construir, +Qty to Fetch,Cantidad a buscar, +Qty to Produce,Cantidad a producir, +Qualification Status,Estado de la cualificación, +Qualified,Cualificado, +Qualified By,Calificado por, +Qualified on,Calificado el, +Quality Inspection Parameter,Parámetros de inspección de calidad, +Quality Inspection Parameter Group,Grupo de parámetros de inspección de calidad, +Quality Inspection Settings,Configuración de inspección de calidad, +Quality Inspection(s),Inspección(es) de calidad, +Quantity is required,Se requiere cantidad, +"Quantity must be greater than zero, and less or equal to {0}",La cantidad debe ser mayor que cero y menor o igual a {0}, +Quantity to Produce should be greater than zero.,La cantidad a producir debe ser mayor que cero., +Quantity to Scan,Cantidad a escanear, +Quarter {0} {1},Cuatrimestre {0} {1}, +Quotation Number,Número de Cotización, +Quoted Amount,Importe Cotizado, +Rate Difference with Purchase Invoice,Diferencia de tarifa con la factura de compra, +Rate Section,Sección de tarifas, +Rate of Stock UOM,Tasa de stock UdM, +Raw Material Cost Per Qty,Coste de la materia prima por cant., +Raw Material Item,Artículo de materia prima, +Raw Material Value,Valor de la materia prima, +Raw Materials Actions,Acciones de Materias Primas, +Raw Materials Consumption ,Consumo de materias primas , +Raw Materials Warehouse,Almacén de materias primas, +Reached Root,Raíz alcanzada, +Reading Value,Valor de lectura, +Reason for hold:,Motivo de la retención:, +Rebuild Tree,Reconstruir el árbol, +Rebuilding BTree for period ...,Reconstruyendo BTree para el período..., +Recalculate Incoming/Outgoing Rate,Recalcular la tasa de entrada/salida, +Recalculating Purchase Cost against this Project...,Recalcular el coste de compra contra este proyecto..., +Receivable/Payable Account,Cuenta por cobrar/por pagar, +Receivable/Payable Account: {0} doesn't belong to company {1},Cuenta por cobrar/por pagar: {0} no pertenece a la empresa {1}, +Received Amount After Tax,Importe recibido después de impuestos, +Received Amount After Tax (Company Currency),Importe recibido después de impuestos (moneda de la empresa), +Received Amount cannot be greater than Paid Amount,El importe recibido no puede ser mayor que el importe pagado, +Received Qty in Stock UOM,Cantidad recibida en stock UdM, +Recent Orders,Pedidos recientes, +Recent Transactions,Transacciones recientes, +Reconcile All Serial Nos / Batches,Concilie todos los números de serie / lotes, +Reconcile on Advance Payment Date,Conciliar en fecha de pago anticipado, +Reconcile the Bank Transaction,Conciliar la transacción bancaria, +Reconciled Entries,Entradas conciliadas, +Reconciliation Error Log,Registro de errores de conciliación, +Reconciliation Logs,Registros de conciliación, +Reconciliation Progress,Progreso de la reconciliación, +Recording HTML,Grabación HTML, +Recoverable Standard Rated expenses should not be set when Reverse Charge Applicable is Y,Los gastos recuperables con tasa estándar no se deben establecer cuando la inversión de cargo aplicable es Y, +Recurse Every (As Per Transaction UOM),Recursiva cada (según la unidad de medida de la transacción), +Recurse Over Qty cannot be less than 0,El recursivo sobre cantidad no puede ser menor que 0, +Recursive Discounts with Mixed condition is not supported by the system,El sistema no admite descuentos recursivos con condiciones mixtas, +Reference Date for Early Payment Discount,Fecha de referencia para el descuento por pronto pago, +Reference Detail,Detalle de referencia, +Reference DocType,DocType de referencia, +Reference Exchange Rate,Tipo de cambio de referencia, +Reference No,Nº de referencia, +Reference number of the invoice from the previous system,Número de referencia de la factura del sistema anterior, +References to Sales Invoices are Incomplete,Las referencias a las facturas de venta están incompletas, +References to Sales Orders are Incomplete,Las referencias a los pedidos de venta están incompletas, +References {0} of type {1} had no outstanding amount left before submitting the Payment Entry. Now they have a negative outstanding amount.,Las referencias {0} del tipo {1} no tenían ningún importe pendiente antes de enviar la Entrada de pago. Ahora tienen un importe pendiente negativo., +Refresh Google Sheet,Actualizar hoja de Google, +Refresh Plaid Link,Actualizar enlace de Plaid, +Regenerate Closing Stock Balance,Regenerar saldo de stock de cierre, +Rejected Serial and Batch Bundle,Lote y serie rechazados, +Rejected Warehouse and Accepted Warehouse cannot be same.,Almacén Rechazado y Almacén Aceptado no pueden ser el mismo., +Remarks Column Length,Observaciones Longitud de la columna, +Remove Parent Row No in Items Table,Eliminar el número de fila principal en la tabla de elementos, +Repair,Reparar, +Repair Asset,Reparar activos, +Repair Details,Detalles de la reparación, +"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. +It also updates latest price in all the BOMs.","Sustituye una determinada lista de materiales en todas las demás listas de materiales en las que se utilice. Reemplazará el enlace de la lista de materiales antigua, actualizará el coste y regenerará la tabla ""Elemento de explosión de la lista de materiales"" según la nueva lista de materiales. +También actualiza el último precio en todas las listas de materiales.", +Report Error,Reportar Error, +Report Filters,Filtros de informe, +Report View,Vista de Reporte, +Repost Accounting Ledger,Traspaso libro de contabilidad, +Repost Accounting Ledger Items,Traspaso de partidas del libro mayor de contabilidad, +Repost Accounting Ledger Settings,Traspasar configuración del libro mayor de contabilidad, +Repost Allowed Types,Tipos de Traspasos permitidos, +Repost Error Log,Traspaso registro de errores, +Repost Item Valuation,Traspaso Valoración de artículos, +Repost Payment Ledger,Traspaso del Libro de Pagos, +Repost Payment Ledger Items,Traspaso de partidas del libro de pagos, +Repost Status,Estado del Traspaso, +Repost has started in the background,El traspaso ha comenzado en segundo plano., +Repost in background,Traspasar en segundo plano, +Repost started in the background,Traspaso iniciado en segundo plano, +Reposting Completed {0}%,Traspaso completado {0}%, +Reposting Data File,Traspaso del archivo de datos, +Reposting Info,Información de Traspaso , +Reposting Progress,Traspasar el progreso, +Reposting entries created: {0},Traspaso de entradas creadas: {0}, +Reposting has been started in the background.,Se ha iniciado un traspaso en segundo plano., +Reposting in the background.,Traspasando en segundo plano., +Represents a Financial Year. All accounting entries and other major transactions are tracked against the Fiscal Year.,Representa un año fiscal. Todos los asientos contables y otras transacciones importantes se registran en relación con el año fiscal., +Request Parameters,Parámetros de la solicitud, +Request Timeout,Tiempo de espera superado, +Reservation Based On,Reserva basada en, +Reserve,Reservar, +Reserve Stock,Reservar stock, +"Reserved Qty ({0}) cannot be a fraction. To allow this, disable '{1}' in UOM {3}.","La cantidad reservada ({0}) no puede ser una fracción. Para permitirlo, deshabilite '{1}' en la unidad de medida {3}.", +Reserved Qty for Production Plan,Cantidad reservada para el plan de producción, +Reserved Qty for Subcontract,Cantidad reservada para subcontrato, +Reserved Qty should be greater than Delivered Qty.,La cantidad reservada debe ser mayor que la cantidad entregada., +Reserved Serial No.,Número de serie reservado., +Reserved Stock,Existencias Reservadas, +Reserved Stock for Batch,Stock reservado para lote, +Reserved for POS Transactions,Reservado para transacciones TPV, +Reserved for Production,Reservado para producción, +Reserved for Production Plan,Reservado para el plan de producción, +Reserved for Sub Contracting,Reservado para subcontratación, +Reserving Stock...,Reservando de stock..., +Reset Company Default Values,Restablecer valores predeterminados de la empresa, +Reset Plaid Link,Restablecer vinculación con Plaid, +Reset Raw Materials Table,Restablecer tabla de materias primas, +Resolution Due,Resolución pendiente, +Response and Resolution,Respuesta y resolución, +Restart,Reiniciar, +Restore Asset,Restaurar activo, +Restrict,Restringir, +Restrict Items Based On,Restringir Pruductos según, +Result Key,Clave de resultados, +Resume Job,Reanudar Trabajo, +Retried,Reintentado, +Retry,Reintentar, +Retry Failed Transactions,Reintentar transacciones fallidas, +Return Against,Devolución contra, +Return Against Subcontracting Receipt,Devolución contra recibo de subcontratación, +Return Components,Componentes de retorno, +Return Issued,Devolución emitida, +Return Qty,Cantidad devuelta, +Return Qty from Rejected Warehouse,Cantidad devuelta del almacén rechazado, +Return of Components,Devolución de componentes, +Returned,Devuelto, +Returned Against,Devuelto contra, +Returned Qty ,Cant devuelta , +Returned Qty in Stock UOM,Cantidad devuelta en stock UdM, +Returned exchange rate is neither integer not float.,El tipo de cambio devuelto no es ni entero ni flotante., +Revaluation Journals,Diarios de revalorización, +Revaluation Surplus,Superávit de revalorización, +Revenue,Ganancia, +Reversal Of,Reversión de, +Right Child,Secundario correcto, +Role Allowed to Create/Edit Back-dated Transactions,Rol permitido para crear o editar transacciones retroactivas, +Role Allowed to Over Bill ,Rol Permitido para Facturar en Exceso , +Role Allowed to Over Deliver/Receive,Rol que permite entregar/recibir más de lo debido, +Role Allowed to Override Stop Action,Rol autorizado para anular la acción de parada, +Role allowed to bypass Credit Limit,Rol permitido para eludir el límite de crédito, +Root,Raíz, +"Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity","El tipo de raíz para {0} debe ser uno de los siguientes: Activo, Pasivo, Ingreso, Gasto y Patrimonio", +Round Free Qty,Redondear cantidad gratis, +Round Off Tax Amount,Redondear el importe del impuesto, +Round Tax Amount Row-wise,Redondear el importe del impuesto por filas, +Rounding Loss Allowance,Redondeo de la indemnización por pérdidas, +Rounding Loss Allowance should be between 0 and 1,El margen de pérdida por redondeo debe estar entre 0 y 1, +Rounding gain/loss Entry for Stock Transfer,Redondeo de ganancias/pérdidas Entrada para traslado de existencias, +Row #,Fila #, +Row # {0}:,Fila # {0}:, +Row #{0}: A reorder entry already exists for warehouse {1} with reorder type {2}.,Fila #{0}: Ya existe una entrada de reorden para el almacén {1} con el tipo de reorden {2}., +Row #{0}: Acceptance Criteria Formula is incorrect.,Fila #{0}: La fórmula de los criterios de aceptación es incorrecta., +Row #{0}: Acceptance Criteria Formula is required.,Fila #{0}: Se requiere la fórmula de criterios de aceptación., +Row #{0}: Accepted Warehouse and Rejected Warehouse cannot be same,Fila #{0}: Almacén Aceptado y Almacén Rechazado no puede ser el mismo, +Row #{0}: Accepted Warehouse is mandatory for the accepted Item {1},Fila #{0}: El almacén aceptado es obligatorio para el artículo aceptado {1}, +Row #{0}: Allocated Amount cannot be greater than Outstanding Amount of Payment Request {1},Fila #{0}: El Importe Asignado no puede ser mayor que el Importe Pendiente de la Solicitud de Pago {1}, +Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3},Fila #{0}: Importe asignado:{1} es superior al importe pendiente:{2} para el plazo de pago {3}, +Row #{0}: Amount must be a positive number,Fila #{0}: El importe debe ser un número positivo, +Row #{0}: BOM is not specified for subcontracting item {0},Fila #{0}: La lista de materiales no está especificada para el artículo de subcontratación {0}, +Row #{0}: Batch No {1} is already selected.,Fila #{0}: El lote nº {1} ya está seleccionado., +Row #{0}: Cannot allocate more than {1} against payment term {2},Fila #{0}: No se puede asignar más de {1} contra la condición de pago {2}, +Row #{0}: Cannot transfer more than Required Qty {1} for Item {2} against Job Card {3},Fila #{0}: No se puede transferir más de la cantidad requerida {1} para el artículo {2} contra la tarjeta de trabajo {3}, +Row #{0}: Consumed Asset {1} cannot be Draft,Fila #{0}: El activo consumido {1} no puede ser borrador, +Row #{0}: Consumed Asset {1} cannot be cancelled,Fila #{0}: El activo consumido {1} no puede estar cancelado, +Row #{0}: Consumed Asset {1} cannot be the same as the Target Asset,Fila #{0}: El activo consumido {1} no puede ser el mismo que el activo de destino, +Row #{0}: Consumed Asset {1} cannot be {2},Fila #{0}: El activo consumido {1} no puede ser {2}, +Row #{0}: Consumed Asset {1} does not belong to company {2},Fila #{0}: El activo consumido {1} no pertenece a la empresa {2}, +Row #{0}: Cumulative threshold cannot be less than Single Transaction threshold,Fila #{0}: El umbral acumulativo no puede ser menor que el umbral de transacción única, +Row #{0}: Dates overlapping with other row,Fila #{0}: Fechas superpuestas con otras filas, +Row #{0}: Default BOM not found for FG Item {1},Fila #{0}: No se encontró la lista de materiales predeterminada para el artículo FG {1}, +Row #{0}: Expense Account not set for the Item {1}. {2},Fila #{0}: Cuenta de gastos no configurada para el artículo {1}. {2}, +Row #{0}: Finished Good Item Qty can not be zero,Fila #{0}: La cantidad de artículos terminados no puede ser cero, +Row #{0}: Finished Good Item is not specified for service item {1},Fila #{0}: No se especifica el artículo bueno acabado para el artículo de servicio {1}, +Row #{0}: Finished Good Item {1} must be a sub-contracted item,Fila #{0}: Artículo terminado. El artículo {1} debe ser un artículo subcontratado., +Row #{0}: Finished Good reference is mandatory for Scrap Item {1}.,Fila #{0}: La referencia de producto terminado es obligatoria para el artículo de desecho {1}., +Row #{0}: For {1} Clearance date {2} cannot be before Cheque Date {3},Fila #{0}: Para {1} La fecha de liquidación {2} no puede ser anterior a la fecha del cheque {3}, +"Row #{0}: For {1}, you can select reference document only if account gets credited","Fila #{0}: Para {1}, puede seleccionar el documento de referencia solo si se acredita la cuenta", +"Row #{0}: For {1}, you can select reference document only if account gets debited","Fila #{0}: Para {1}, puede seleccionar el documento de referencia solo si se debita la cuenta", +Row #{0}: From Date cannot be before To Date,Fila #{0}: La fecha de inicio no puede ser anterior a la fecha de finalización, +Row #{0}: Item {1} does not exist,Fila #{0}: El artículo {1} no existe, +"Row #{0}: Item {1} has been picked, please reserve stock from the Pick List.","Fila #{0}: El artículo {1} ha sido recogido, por favor reserve existencias de la Lista de Recogida.", +Row #{0}: Item {1} is not a service item,Fila #{0}: El artículo {1} no es un artículo de servicio, +Row #{0}: Item {1} is not a stock item,Fila #{0}: El artículo {1} no es un artículo de stock, +Row #{0}: Only {1} available to reserve for the Item {2},Fila #{0}: Solo {1} disponible para reservar para el artículo {2}, +Row #{0}: Please select Item Code in Assembly Items,"Fila #{0}: Por favor, seleccione el código del artículo en Artículos de ensamblaje", +Row #{0}: Please select the BOM No in Assembly Items,"Fila #{0}: Por favor, seleccione el nº de lista de materiales en Artículos de ensamblaje", +Row #{0}: Please select the Sub Assembly Warehouse,"Fila #{0}: Por favor, seleccione el Almacén de Sub-montaje", +Row #{0}: Please update deferred revenue/expense account in item row or default account in company master,"Fila #{0}: Por favor, actualice la cuenta de ingresos/gastos diferidos en la fila de artículos o la cuenta por defecto en el maestro de empresas", +Row #{0}: Qty increased by {1},Fila #{0}: Cantidad aumentada en {1}, +Row #{0}: Qty must be a positive number,Fila #{0}: La cantidad debe ser un número positivo, +Row #{0}: Qty should be less than or equal to Available Qty to Reserve (Actual Qty - Reserved Qty) {1} for Iem {2} against Batch {3} in Warehouse {4}.,Fila #{0}: La cantidad debe ser menor o igual a la cantidad disponible para reservar (cantidad real - cantidad reservada) {1} para Artículo {2} contra el lote {3} en el almacén {4}., +Row #{0}: Quantity to reserve for the Item {1} should be greater than 0.,Fila #{0}: La cantidad a reservar para el artículo {1} debe ser superior a 0., +Row #{0}: Rate must be same as {1}: {2} ({3} / {4}),Fila #{0}: La tasa debe ser la misma que {1}: {2} ({3} / {4}), +Row #{0}: Received Qty must be equal to Accepted + Rejected Qty for Item {1},Fila #{0}: La cantidad recibida debe ser igual a la cantidad aceptada + rechazada para el artículo {1}, +Row #{0}: Rejected Qty cannot be set for Scrap Item {1}.,Fila #{0}: No se puede establecer la cantidad rechazada para el artículo de rechazo {1}., +Row #{0}: Rejected Warehouse is mandatory for the rejected Item {1},Fila #{0}: El almacén rechazado es obligatorio para el artículo rechazado {1}, +Row #{0}: Scrap Item Qty cannot be zero,Fila #{0}: La cantidad de artículos desechados no puede ser cero, +"Row #{0}: Selling rate for item {1} is lower than its {2}. + Selling {3} should be atleast {4}.

Alternatively, + you can disable selling price validation in {5} to bypass + this validation.","Fila #{0}: El precio de venta del artículo {1} es inferior a su {2}. + Venta {3} debe ser al menos {4}.

Alternativamente, + puede desactivar la validación del precio de venta en {5} para saltarse + esta validación.", +Row #{0}: Serial No {1} for Item {2} is not available in {3} {4} or might be reserved in another {5}.,Fila #{0}: El número de serie {1} del artículo {2} no está disponible en {3} {4} o podría estar reservado en otro {5}., +Row #{0}: Serial No {1} is already selected.,Fila #{0}: El número de serie {1} ya está seleccionado., +Row #{0}: Start Time and End Time are required,Fila #{0}: Se requiere hora de inicio y hora de fin, +Row #{0}: Start Time must be before End Time,Fila #{0}: La hora de inicio debe ser antes del fin, +Row #{0}: Status is mandatory,Fila #{0}: El estado es obligatorio, +Row #{0}: Stock cannot be reserved for Item {1} against a disabled Batch {2}.,Fila #{0}: No se puede reservar stock para el artículo {1} contra un lote deshabilitado {2}., +Row #{0}: Stock cannot be reserved for a non-stock Item {1},Fila #{0}: No se puede reservar stock para un artículo que no es de stock {1}, +Row #{0}: Stock cannot be reserved in group warehouse {1}.,Fila #{0}: No se pueden reservar existencias en el almacén de grupo {1}., +Row #{0}: Stock is already reserved for the Item {1}.,Fila #{0}: Ya hay stock reservado para el artículo {1}., +Row #{0}: Stock is reserved for item {1} in warehouse {2}.,Fila #{0}: Hay stock reservado para el artículo {1} en el almacén {2}., +Row #{0}: Stock not available to reserve for Item {1} against Batch {2} in Warehouse {3}.,Fila #{0}: Stock no disponible para reservar para el artículo {1} contra el lote {2} en el almacén {3}., +Row #{0}: Stock not available to reserve for the Item {1} in Warehouse {2}.,Fila #{0}: Stock no disponible para reservar para el artículo {1} en el almacén {2}., +Row #{0}: The warehouse {1} is not a child warehouse of a group warehouse {2},Fila #{0}: El almacén {1} no es un almacén secundario de un almacén de grupo {2}, +Row #{0}: You cannot use the inventory dimension '{1}' in Stock Reconciliation to modify the quantity or valuation rate. Stock reconciliation with inventory dimensions is intended solely for performing opening entries.,Fila #{0}: No se puede utilizar la dimensión de inventario '{1}' en la conciliación de stock para modificar la cantidad o la tasa de valoración. La conciliación de stock con las dimensiones de inventario está destinada únicamente a realizar asientos de apertura., +Row #{0}: You must select an Asset for Item {1}.,Fila #{0}: Debe seleccionar un activo para el artículo {1}., +Row #{0}: {1} is not a valid reading field. Please refer to the field description.,Fila #{0}: {1} no es un campo de lectura válido. Consulte la descripción del campo., +Row #{0}: {1} of {2} should be {3}. Please update the {1} or select a different account.,"Fila #{0}: {1} de {2} debería ser {3}. Por favor, actualice {1} o seleccione una cuenta diferente.", +Row #{1}: Warehouse is mandatory for stock Item {0},Fila #{1}: El Almacén es obligatorio para el producto en stock {0}, +Row #{}: Finance Book should not be empty since you're using multiple.,"Fila #{}: Libro de Finanzas no debe estar vacío, ya que está utilizando múltiples.", +Row #{}: Please use a different Finance Book.,"Fila #{}: Por favor, utilice un Libro de Finanzas diferente.", +Row #{}: The original Invoice {} of return invoice {} is not consolidated.,Fila #{}: La factura original {} de la factura de devolución {} no está consolidada., +Row #{}: item {} has been picked already.,Fila #{}: el artículo {} ya ha sido seleccionado., +Row #{}: {} {} doesn't belong to Company {}. Please select valid {}.,"Fila #{}: {} {} no pertenece a la empresa {}. Por favor, seleccione una {} válida.", +Row No {0}: Warehouse is required. Please set a Default Warehouse for Item {1} and Company {2},Fila n.° {0}: Se requiere almacén. Establezca un almacén predeterminado para el artículo {1} y la empresa {2}, +Row Number,Número de fila, +Row {0},Fila {0}, +"Row {0} picked quantity is less than the required quantity, additional {1} {2} required.","Fila {0} la cantidad recogida es inferior a la requerida, se requiere {1} {2} adicional.", +Row {0}# Item {1} cannot be transferred more than {2} against {3} {4},Fila {0}# El artículo {1} no puede transferirse más que {2} contra {3} {4}, +Row {0}# Item {1} not found in 'Raw Materials Supplied' table in {2} {3},Fila {0}# El artículo {1} no se encontró en la tabla 'Materias primas suministradas' en {2} {3}, +Row {0}: Accepted Qty and Rejected Qty can't be zero at the same time.,Fila {0}: La cantidad aceptada y la cantidad rechazada no pueden ser cero al mismo tiempo., +Row {0}: Account {1} and Party Type {2} have different account types,Fila {0}: La cuenta {1} y el tipo de entidad {2} tienen diferentes tipos de cuenta, +Row {0}: Allocated amount {1} must be less than or equal to invoice outstanding amount {2},Fila {0}: El importe asignado {1} debe ser inferior o igual al importe pendiente de la factura {2}, +Row {0}: Allocated amount {1} must be less than or equal to remaining payment amount {2},Fila {0}: El importe asignado {1} debe ser inferior o igual al importe de pago restante {2}, +"Row {0}: As {1} is enabled, raw materials cannot be added to {2} entry. Use {3} entry to consume raw materials.","Fila {0}: Como {1} está activada, no se pueden añadir materias primas a la entrada {2} . Utilice la entrada {3} para consumir materias primas.", +Row {0}: Both Debit and Credit values cannot be zero,Fila {0}: Tanto el Debe como el Haber no pueden ser cero, +Row {0}: Cost Center {1} does not belong to Company {2},Fila {0}: El centro de costes {1} no pertenece a la empresa {2}, +Row {0}: Either Delivery Note Item or Packed Item reference is mandatory.,Fila {0}: La referencia del artículo de la nota de entrega o del artículo empaquetado es obligatoria., +Row {0}: Expense Head changed to {1} as no Purchase Receipt is created against Item {2}.,Fila {0}: el encabezado de gasto cambió a {1} ya que no se crea ningún recibo de compra para el artículo {2}., +Row {0}: Expense Head changed to {1} because account {2} is not linked to warehouse {3} or it is not the default inventory account,Fila {0}: Cabecera de Gasto cambiada a {1} porque la cuenta {2} no está vinculada al almacén {3} o no es la cuenta de inventario por defecto, +Row {0}: Expense Head changed to {1} because expense is booked against this account in Purchase Receipt {2},Fila {0}: Cabecera de Gasto cambiada a {1} porque el gasto se contabiliza contra esta cuenta en el Recibo de Compra {2}, +Row {0}: From Warehouse is mandatory for internal transfers,Fila {0}: Desde el almacén es obligatorio para transferencias internas, +Row {0}: Item Tax template updated as per validity and rate applied,Fila {0}: Plantilla de impuesto del artículo actualizada según la validez y la tasa aplicada, +Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer,"Fila {0}: La tarifa del artículo se ha actualizado según la tarifa de valoración, ya que se trata de una transferencia de stock interna", +Row {0}: Item {1} must be a stock item.,Fila {0}: El artículo {1} debe ser un artículo de stock., +Row {0}: Item {1} must be a subcontracted item.,Fila {0}: El artículo {1} debe ser un artículo subcontratado., +Row {0}: Packed Qty must be equal to {1} Qty.,Fila {0}: La cantidad embalada debe ser igual a la cantidad {1} ., +Row {0}: Packing Slip is already created for Item {1}.,Fila {0}: Ya se creó el albarán para el artículo {1}., +Row {0}: Payment Term is mandatory,Fila {0}: El plazo de pago es obligatorio, +Row {0}: Please provide a valid Delivery Note Item or Packed Item reference.,Fila {0}: proporcione una referencia de artículo de nota de entrega o artículo empaquetado válida., +Row {0}: Please select a BOM for Item {1}.,"Fila {0}: Por favor, seleccione una lista de materiales para el artículo {1}.", +Row {0}: Please select an active BOM for Item {1}.,"Fila {0}: Por favor, seleccione una lista de materiales activa para el artículo {1}.", +Row {0}: Please select an valid BOM for Item {1}.,"Fila {0}: Por favor, seleccione una lista de materiales válida para el artículo {1}.", +Row {0}: Project must be same as the one set in the Timesheet: {1}.,Fila {0}: El proyecto debe ser el mismo que el establecido en la hoja de horas: {1}., +Row {0}: Purchase Invoice {1} has no stock impact.,Fila {0}: La factura de compra {1} no tiene impacto en el stock., +Row {0}: Qty cannot be greater than {1} for the Item {2}.,Fila {0}: La cantidad no puede ser mayor que {1} para el artículo {2}., +Row {0}: Qty in Stock UOM can not be zero.,Fila {0}: La UdM de cantidad en stock no puede ser cero., +Row {0}: Qty must be greater than 0.,Fila {0}: La cantidad debe ser mayor que 0., +Row {0}: Shift cannot be changed since the depreciation has already been processed,Fila {0}: No se puede cambiar el turno porque ya se ha procesado la amortización, +Row {0}: Target Warehouse is mandatory for internal transfers,Fila {0}: El almacén de destino es obligatorio para las transferencias internas, +"Row {0}: To set {1} periodicity, difference between from and to date must be greater than or equal to {2}","Fila {0}: Para establecer la periodicidad {1} , la diferencia entre la fecha de inicio y la de finalización debe ser mayor o igual a {2}", +Row {0}: Total Number of Depreciations cannot be less than or equal to Opening Number of Booked Depreciations,Fila {0}: El número total de amortizaciones no puede ser inferior o igual al número inicial de amortizaciones contabilizadas, +Row {0}: {1} account already applied for Accounting Dimension {2},Fila {0}: {1} cuenta ya aplicada para la Dimensión Contable {2}, +Row {0}: {1} {2} cannot be same as {3} (Party Account) {4},Fila {0}: {1} {2} no puede ser la misma que {3} (Cuenta de la Entidad) {4}, +Row {0}: {2} Item {1} does not exist in {2} {3},Fila {0}: {2} El elemento {1} no existe en {2} {3}, +Row({0}): Outstanding Amount cannot be greater than actual Outstanding Amount {1} in {2},Fila({0}): El importe pendiente no puede ser mayor que el importe pendiente real {1} en {2}, +Rows with Same Account heads will be merged on Ledger,Las líneas con los mismos encabezamientos de cuenta se fusionarán en el Libro Mayor, +Rows: {0} have 'Payment Entry' as reference_type. This should not be set manually.,Filas: {0} tienen 'Entrada de pago' como reference_type. No debe establecerse manualmente., +Rows: {0} in {1} section are Invalid. Reference Name should point to a valid Payment Entry or Journal Entry.,Las filas {0} en la sección {1} no son válidas. El nombre de referencia debe apuntar a una entrada de pago o de diario válida., +Run parallel job cards in a workstation,Ejecutar tarjetas de trabajo en paralelo en una estación de trabajo, +Running,Ejecutando, +SCO Supplied Item,Artículo suministrado por SCO, +SLA Fulfilled On,SLA completado el, +SLA Fulfilled On Status,SLA cumplido en estado, +SLA Paused On,SLA en pausa, +SLA will be applied if {1} is set as {2}{3},SLA se aplicará si {1} se establece como {2}{3}, +SLA will be applied on every {0},El SLA se aplicará en cada {0}, +SMS Settings,Ajustes de SMS, +SO Total Qty,SO Cantidad total, +Salary Currency,Divisa de salario, +Sales Incoming Rate,Tasa de entrada de ventas, +Sales Invoice {0} must be deleted before cancelling this Sales Order,La factura de venta {0} debe eliminarse antes de cancelar esta orden de venta, +Sales Order Packed Item,Artículo empaquetado de la orden de venta, +Sales Order Reference,Referencia del pedido de venta, +Sales Order Status,Estado del pedido de venta, +"Sales Order {0} already exists against Customer's Purchase Order {1}. To allow multiple Sales Orders, Enable {2} in {3}","El Pedido de Venta {0} ya existe contra el Pedido de Compra del Cliente {1}. Para permitir múltiples Pedidos de Venta, habilite {2} en {3}.", +Sales Partner ,Socio de ventas , +Sales Partner Item,Artículo de socio de ventas, +Sales Partner Target Variance Based On Item Group,Variación de objetivos del socio de ventas según el grupo de artículos, +Sales Pipeline Analytics,Análisis del flujo de ventas, +Sales Update Frequency in Company and Project,Frecuencia de actualización de ventas en empresa y proyecto, +Sales Value,Valor de las ventas, +Salvage Value Percentage,Porcentaje de Valor de Recuperación, +Same item and warehouse combination already entered.,Ya se ha introducido la misma combinación de artículo y almacén., +Savings,Ahorros, +Scan Batch No,Escanear Lote No, +Scan Mode,Modo de escaneo, +Scan Serial No,Escanear número de serie, +Scan barcode for item {0},Escanee el código de barras del artículo {0}, +"Scan mode enabled, existing quantity will not be fetched.","Modo de escaneo habilitado, la cantidad existente no se obtendrá.", +Scanned Quantity,Cantidad escaneada, +Scheduled Time Logs,Registros de tiempo programado, +Scheduler is Inactive. Can't trigger job now.,El planificador está inactivo. No se puede activar el trabajo ahora., +Scheduler is Inactive. Can't trigger jobs now.,El planificador está inactivo. No se pueden activar los trabajos ahora., +Scheduler is inactive. Cannot enqueue job.,El planificador está inactivo. No se puede poner en cola el trabajo., +Scheduler is inactive. Cannot merge accounts.,El planificador está inactivo. No se pueden combinar cuentas., +Scheduling,Planificación, +Select Alternative Items for Sales Order,Seleccionar ítems alternativos para Orden de Venta, +Select View,Seleccione Vista, +Select an item from each set to be used in the Sales Order.,Seleccione un ítem de cada conjunto para usarlo en la Orden de Venta., +Send Attached Files,Enviar Archivos Adjuntos, +Send Document Print,Enviar Impresión de Documento, +Service Expenses,Gastos de servicio, +Service Item,Artículo de servicio, +Set Default Supplier,Establecer Proveedor Predeterminado, +Set Quantity,Establecer cantidad, +Set Warehouse,Establecer Almacén, +Shipment,Envío, +Shipment Amount,Monto del envío, +Shipment Delivery Note,Nota de Entrega de envío, +Shipment ID,ID de Envío, +Shipment Information,Información del Envío, +Shipment Type,Tipo de Envío, +Shipment details,Detalles del envío, +Show Failed Logs,Mostrar registros fallidos, +Show Preview,Mostrar Vista Previa, +Show Remarks,Mostrar Observaciones, +Skipped,Omitido, +"Skipping {0} of {1}, {2}","Saltando {0} de {1}, {2}", +Sold by,Vendido por, +Spacer,Espaciador, +Start Deletion,Iniciar eliminación, +Start Import,Comience a Importar, +Sub Operation,Sub operación, +Sub Operations,Sub operaciones, +Subcontracting Settings,Configuración de Subcontratación, +Subdivision,Subdivisión, +Submit After Import,Validar después de la importación, +Successfully imported {0} record.,Importado correctamente {0} registro., +Successfully imported {0} records.,Importado correctamente {0} registros., +Successfully linked to Customer,Vinculado exitosamente al Cliente, +Successfully linked to Supplier,Vinculado exitosamente al Proveedor, +Successfully merged {0} out of {1}.,Fusionado satisfactoriamente {0} de {1}., +Successfully updated {0},Actualizado exitosamente {0}, +"Successfully updated {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.","Registro {0} actualizado correctamente de {1}. Haga clic en Exportar filas con errores, corrija los errores e importe nuevamente.", +Successfully updated {0} record.,Registro {0} actualizado correctamente., +"Successfully updated {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.","Actualizado correctamente los registros {0} de {1}. Haga clic en Exportar filas Erroradas, corrija los errores e importe de nuevo.", +Successfully updated {0} records.,Registros {0} actualizados correctamente., +Supplied Item,Producto suministrado, +Supplier Address Details,Detalles de Dirección del Proveedor, +Supplier Info,Info. Proveedor, +Supplier Invoice,Factura de Proveedor, +Supplier Item,Producto del Proveedor, +Supplier Portal Users,Usuarios del Portal del Proveedor, +Supplier Primary Address,Dirección principal del Proveedor, +Supplier Primary Contact,Contacto principal del Proveedor, +Sync Now,Sincronizar ahora, +Sync Started,Sincronización Iniciada, +System Settings,Configuración del Sistema, +Tax Amount,Importe de Impuestos, +Tax Masters,Maestros Fiscales, +Tax Settings,Configuración de Impuestos, +Team,Equipo, +Template Options,Opciones de plantilla, +Template Warnings,Advertencias de plantilla, +Terms & Conditions,Términos y Condiciones, +Terms Template,Plantilla de Términos, +Territory Wise Sales,Ventas por territorios, +The Condition '{0}' is invalid,La Condición '{0}' no es válida, +"The sync has started in the background, please check the {0} list for new records.","La sincronización se ha iniciado en segundo plano, por favor compruebe en la lista {0} si hay nuevos registros.", +The task has been enqueued as a background job.,La tarea se ha puesto en cola como trabajo en segundo plano., +There was an error creating Bank Account while linking with Plaid.,Se ha producido un error al crear la cuenta bancaria mientras se vinculaba con Plaid., +There was an error syncing transactions.,Se ha producido un error al sincronizar las transacciones., +There was an error updating Bank Account {} while linking with Plaid.,Se ha producido un error al actualizar la cuenta bancaria {} mientras se vinculaba con Plaid., +There was an issue connecting to Plaid's authentication server. Check browser console for more information,Se ha producido un problema al conectar con el servidor de autenticación de Plaid. Compruebe la consola del navegador para obtener más información, +Time in mins,Tiempo en min, +Time in mins.,Tiempo en minutos., +Time slot is not available,La franja horaria no está disponible, +To Doctype,A Doctype, +Total Incoming Value (Receipt),Valor Total Entrante (Recepción), +Total Issues,Total de Incidencias, +Total Outgoing Value (Consumption),Valor Total Saliente (Consumo), +Total Supplied Qty,Cant. Total suministrada, +Total Time (in Mins),Tiempo total (en minutos), +Total Value,Valor Total, +Total Value Difference (Incoming - Outgoing),Diferencia de valor total (entrante - saliente), +Total Views,Total de visualizaciones, +Total Warehouses,Total Almacenes, +Tracking Status,Estado de seguimiento, +Tracking Status Info,Información de estado de seguimiento, +Tracking URL,URL de Seguimiento, +Transaction Settings,Configuración de Transacciones, +Transaction currency: {0} cannot be different from Bank Account({1}) currency: {2},Moneda de la transacción: {0} no puede ser diferente de la moneda de la cuenta bancaria ({1}): {2}, +Transit,Tránsito, +Transit Entry,Entrada de Tránsito, +Type of Transaction,Tipo de Transacción, +UAE VAT 201,IVA EAU 201, +Unable to find variable:,No se puede encontrar la variable:, +Unassigned Qty,Cant. Sin asignar, +Unit of Measure (UOM),Unidad de Medida (UdM), +Unlinked,Desvincular, +Unqualified,No calificada, +Unrealized Profit / Loss Account,Cuenta de Pérdidas/Ganancias no realizada, +Up,Arriba, +Update Existing Records,Actualizar registros existentes, +Update Rate as per Last Purchase,Actualizar tasa según la última compra, +Update Total Purchase Cost,Actualizar Costo Total de Compra, +Updating Work Order status,Actualizando estado de la Orden de Trabajo, +"Updating {0} of {1}, {2}","Actualización {0} de {1}, {2}", +Upload Bank Statement,Cargar extracto bancario, +Use Company Default Round Off Cost Center,Utilizar el Centro de Costes de redondeo por defecto de la Compañía, +Use Company default Cost Center for Round off,Utilizar el Centro de Costos por defecto de la compañía para el redondeo, +Use HTTP Protocol,Usar protocolo HTTP, +Use Transaction Date Exchange Rate,Usar el tipo de cambio de fecha de la transacción, +User {0}: Removed Employee Self Service role as there is no mapped employee.,"Usuario {0}: Eliminado el rol de Autoservicio del Empleado, ya que no hay ningún empleado mapeado.", +User {0}: Removed Employee role as there is no mapped employee.,"Usuario {0}: Se eliminó el rol de Empleado, ya que no hay ningún empleado asignado.", +VAT Accounts,Cuentas de IVA, +VAT Amount (AED),Importe del IVA (AED), +VAT Audit Report,Informe de auditoría del IVA, +Valuation Rate (In / Out),Tasa de Valoración (Entrada/Salida), +Value Change,Cambio de Valor, +Voucher,Comprobante, +"WARNING: Exotel app has been separated from ERPNext, please install the app to continue using Exotel integration.",ADVERTENCIA: La aplicación Exotel se ha separado de ERPNext; instale la aplicación para continuar usando la integración de Exotel., +Waiting for payment...,Esperando Pago..., +Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} {2}.,Capacidad del Almacén para el Producto '{0}' debe ser mayor que el nivel de stock existente de {1} {2}., +Warehouse Details,Detalle del Almacén, +Warehouse Disabled?,¿Almacén deshabilitado?, +Warehouse Settings,Configuración del Almacén, +Warehouse Wise Stock Balance,Saldo de existencias en almacén, +Warehouse wise Stock Value,Valor de las existencias en función del almacén, +Warehouse {0} does not belong to Company {1}.,Almacén {0} no pertenece a la Compañía {1}., +Warning!,¡Advertencia!, +Watch Video,Ver video, +Website Script,Script del Sitio Web, +Website Theme,Tema del Sitio Web, +Week {0} {1},Semana {0} {1}, +Weight (kg),Peso (kg), +Width (cm),Ancho (cm), +Workflow,Flujos de Trabajo, +Workflow Action,Acciones de flujos de trabajo, +Workflow State,Estados de flujos de trabajo, +Workstation Status,Estado de la estación de trabajo, +Workstation Type,Tipo de estación de trabajo, +Workstations,Estación de trabajo, +Wrong Company,Compañía incorrecta, +You haven't created a {0} yet,Aún no ha creado un {0}, +Your Name (required),Su nombre (requerido), +`Allow Negative rates for Items`,`Permitir precios Negativos para los Productos`, +description,descripción, +fieldname,nombre del campo, +variance,variación, +{0} and {1},{0} y {1}, +{0} is already running for {1},{0} ya se está ejecutando por {1}, +{0} {1} Manually,{0} {1} Manualmente, +{0} {1} Partially Reconciled,{0} {1} Parcialmente reconciliado, +{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions,{0} {1}: La cuenta {2} es una Cuenta de Grupo y las Cuentas de Grupo no pueden utilizarse en transacciones, +{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions,{0} {1}: El Centro de Costos {2} es un Centro de Costos de Grupo y los Centros de Costos de Grupo no pueden utilizarse en transacciones, +{0}% of total invoice value will be given as discount.,{0}% del valor total de la factura se otorgará como descuento., +{} Available,{} Disponible, +{} Assigned,{} Asignado, +{} Available,{} Disponible, +{} Open,{} Abierto, +{} {} is already linked with another {},{} {} ya está vinculado con otro {}, +{} {} is already linked with {} {},{} {} ya está vinculado con {} {}, diff --git a/erpnext/translations/fa.csv b/erpnext/translations/fa.csv index 7c2e8544c9c7..f65226bf761e 100644 --- a/erpnext/translations/fa.csv +++ b/erpnext/translations/fa.csv @@ -8743,3 +8743,2535 @@ WhatsApp,واتس اپ, Make a call,تماس بگیر, Approve,تایید, Reject,رد کردن, + Address, آدرس, + Amount, میزان, + Is Child Table, جدول فرزند است, + Name, نام, + Rate, نرخ, + Summary, خلاصه, +"""SN-01::10"" for ""SN-01"" to ""SN-10""","""SN-01::10"" برای ""SN-01"" تا ""SN-10""", +# In Stock,# در موجودی, +# Req'd Items,# آیتم‌های درخواست شده, +% Finished Item Quantity,% مقدار آیتم تمام شده, +% Occupied,٪ مشغول, +% Picked,% انتخاب شده, +% Process Loss,% از دست دادن فرآیند, +% Returned,% برگردانده شده, +'Account' in the Accounting section of Customer {0},حساب در بخش حسابداری مشتری {0}, +'Allow Multiple Sales Orders Against a Customer's Purchase Order',اجازه دادن سفارش‌های فروش چندگانه در برابر سفارش خرید مشتری, +'Default {0} Account' in Company {1},«حساب پیش‌فرض {0}» در شرکت {1}, +'To Package No.' cannot be less than 'From Package No.',"'به شماره بسته.' نمی تواند کمتر از ""از شماره بسته"" باشد.", +'{0}' account is already used by {1}. Use another account.,حساب '{0}' قبلاً توسط {1} استفاده شده است. از حساب دیگری استفاده کنید., +'{0}' should be in company currency {1}.,"""{0}"" باید به ارز شرکت {1} باشد.", +(A) Qty After Transaction,(A) تعداد پس از تراکنش, +(B) Expected Qty After Transaction,(ب) تعداد مورد انتظار پس از تراکنش, +(C) Total Qty in Queue,(C) تعداد کل در صف, +(C) Total qty in queue,(C) تعداد کل در صف, +(D) Balance Stock Value,(D) ارزش موجودی, +(E) Balance Stock Value in Queue,(E) موجودی ارزش موجودی در صف, +(F) Change in Stock Value,(F) تغییر در ارزش موجودی, +(G) Sum of Change in Stock Value,(ز) مجموع تغییر در ارزش موجودی, +(H) Change in Stock Value (FIFO Queue),(H) تغییر در ارزش موجودی (صف FIFO), +(H) Valuation Rate,(H) نرخ ارزش گذاری, +(I) Valuation Rate,(I) نرخ ارزش گذاری, +(J) Valuation Rate as per FIFO,(J) نرخ ارزیابی مطابق با FIFO, +(K) Valuation = Value (D) ÷ Qty (A),(K) ارزش = ارزش (D) ÷ تعداد (A), +", with the inventory {0}: {1}",، با موجودی {0}: {1}, +0-30 Days,0-30 روز, +3 Yearly,3 سالانه, +30-60 Days,30-60 روز, +60-90 Days,60-90 روز, +90 Above,90 بالا, +"
Other Details
","
جزئیات دیگر
", +"
No Matching Bank Transactions Found
","
هیچ تراکنش بانکی منطبقی پیدا نشد
", +"
+

All dimensions in centimeter only

+
","
+

همه ابعاد فقط به سانتی‌متر

+
", +"

About Product Bundle

+ +

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.

+

The package Item will have Is Stock Item as No and Is Sales Item as Yes.

+

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.

","

درباره باندل محصول

+ +

گروهی از آیتم‌ها را در آیتم دیگری جمع آوری کنید. اگر آیتم‌های خاصی را در یک باندل قرار دهید و موجودی آیتم‌های بسته بندی شده را حفظ کنید و نه آیتم‌های جمع آوری شده، مفید است.

+

بسته آیتم دارای آیتم موجودی است به عنوان خیر و آیتم فروش است به عنوان بله خواهد بود. .

+

مثال:

+

اگر لپ‌تاپ و کوله‌پشتی را جداگانه می‌فروشید و اگر مشتری هر دو را بخرد، قیمت ویژه‌ای دارید، لپ‌تاپ + کوله‌پشتی یک کالای باندل محصول جدید خواهد بود.

", +"
Or
","
یا
", +"","", +"","", +"","", +"Masters & Reports","مستندات و گزارش ها", +"Quick Access","دسترسی سریع", +"Reports & Masters","گزارش ها و مستندات", +"Reports & Masters","گزارش ها و مستندات", +"Settings","تنظیمات", +"Shortcuts","میانبرها", +"Your Shortcuts + + + + + + ","میانبرهای شما + + + + + + ", +"Your Shortcuts","میانبرهای شما", +Grand Total: {0},جمع کل: {0}, +Outstanding Amount: {0},مبلغ معوق: {0}, +A - B,الف - ب, +A - C,الف - ج, +A Holiday List can be added to exclude counting these days for the Workstation.,فهرست تعطیلات را می توان اضافه کرد تا شمارش این روزها برای ایستگاه کاری حذف شود., +A Packing Slip can only be created for Draft Delivery Note.,یک برگه بسته بندی فقط می تواند برای پیش نویس یادداشت تحویل ایجاد شود., +"A Price List is a collection of Item Prices either Selling, Buying, or both",لیست قیمت مجموعه ای از قیمت های آیتم‌های فروش، خرید یا هر دو است, +A Reconciliation Job {0} is running for the same filters. Cannot reconcile now,یک کار تطبیق {0} برای همین فیلترها در حال اجرا است. الان نمیشه تطبیق کرد, +A Transaction Deletion Document: {0} is triggered for {0},یک سند حذف تراکنش: {0} برای {0} فعال می شود, +A customer must have primary contact email.,مشتری باید ایمیل تماس اصلی داشته باشد., +A driver must be set to submit.,یک راننده باید برای ارسال تنظیم شود., +A template with tax category {0} already exists. Only one template is allowed with each tax category,الگویی با دسته مالیاتی {0} از قبل وجود دارد. فقط یک الگو با هر دسته مالیات مجاز است, +API Details,جزئیات API, +AWB Number,شماره AWB, +Abbreviation: {0} must appear only once,مخفف: {0} باید فقط یک بار ظاهر شود, +About Us Settings,تنظیمات درباره ما, +About {0} minute remaining,حدود {0} دقیقه باقی مانده است, +About {0} minutes remaining,حدود {0} دقیقه باقی مانده است, +About {0} seconds remaining,حدود {0} ثانیه باقی مانده است, +Acceptance Criteria Formula,فرمول معیارهای پذیرش, +Acceptance Criteria Value,مقدار معیارهای پذیرش, +Accepted Qty in Stock UOM,تعداد پذیرفته شده در انبار UOM, +Access Key,کلید دسترسی, +Access Key is required for Service Provider: {0},کلید دسترسی برای ارائه‌دهنده خدمات لازم است: {0}, +Account Balance (From),تراز حساب (از), +Account Balance (To),تراز حساب (به), +Account Closing Balance,تراز اختتامیه حساب, +Account Currency (From),ارز حساب (از), +Account Currency (To),ارز حساب (به), +Account Opening Balance,تراز افتتاحیه حساب, +Account not Found,حساب پیدا نشد, +Account {0} added multiple times,حساب {0} چندین بار اضافه شد, +Accounting Dimension Filter,فیلتر ابعاد حسابداری, +Accounting Dimensions Filter,فیلتر ابعاد حسابداری, +Accounting Entry for {0},ثبت حسابداری برای {0}, +Accounts Closing,بسته شدن حساب ها, +Accounts Missing Error,خطای گم شدن حساب ها, +Accounts Receivable/Payable,حساب های دریافتنی / پرداختنی, +Accounts to Merge,حساب ها برای ادغام, +Action If Quality Inspection Is Rejected,اقدام اگر بازرسی کیفیت رد شود, +Action If Same Rate is Not Maintained,اگر همان نرخ حفظ نشود، اقدام کنید, +Action if Same Rate is Not Maintained Throughout Sales Cycle,اگر نرخ یکسانی در طول چرخه فروش حفظ نشود، اقدام کنید, +Active Status,وضعیت فعال, +Actual Balance Qty,مقدار تراز واقعی, +Actual Expense,هزینه واقعی, +Actual Posting,ارسال واقعی, +Actual Qty in Warehouse,مقدار واقعی در انبار, +Actual Time,زمان واقعی, +Add Columns in Transaction Currency,اضافه کردن ستون به ارز تراکنش, +Add Corrective Operation Cost in Finished Good Valuation,اضافه کردن هزینه عملیات اصلاحی در ارزش گذاری کالای تمام شده, +Add Discount,افزودن تخفیف, +Add Items in the Purpose Table,افزودن آیتم‌ها در جدول هدف, +Add Lead to Prospect,لید را به Prospect اضافه کنید, +Add Local Holidays,تعطیلات محلی را اضافه کنید, +Add Manually,افزودن دستی, +Add Or Deduct,افزودن یا کسر, +Add Serial / Batch Bundle,افزودن باندل سریال / دسته, +Add Serial / Batch No,اضافه کردن سریال / شماره دسته, +Add Serial / Batch No (Rejected Qty),اضافه کردن سریال / شماره دسته (تعداد رد شده), +Add Stock,افزودن موجودی, +Add Sub Assembly,افزودن زیر مونتاژ, +Add Template,اضافه کردن الگو, +Add a Note,یک یادداشت اضافه کنید, +Add details,جزئیات را اضافه کنید, +Add to Prospect,به Prospect اضافه کنید, +Added By,اضافه شده توسط, +Added On,اضافه شده در, +Added Supplier Role to User {0}.,نقش تامین کننده به کاربر {0} اضافه شد., +Added {1} Role to User {0}.,نقش {1} به کاربر {0} اضافه شد., +Adding Lead to Prospect...,افزودن سرنخ به مشتری بالقوه..., +Additional,اضافی, +Additional Asset Cost,هزینه دارایی اضافی, +Additional Cost Per Qty,هزینه اضافی در هر تعداد, +Additional Info,اطلاعات اضافی, +Address And Contacts,آدرس و مخاطبین, +Adjust Asset Value,ارزش دارایی را تنظیم کنید, +Adjustment Against,تعدیل در مقابل, +Adjustment based on Purchase Invoice rate,تعدیل بر اساس نرخ فاکتور خرید, +Advance Payment,پیش پرداخت, +Advance Tax,پیش پرداخت مالیات, +Advance Taxes and Charges,پیش پرداخت مالیات و هزینه ها, +Advance paid against {0} {1} cannot be greater than Grand Total {2},پیش پرداخت در مقابل {0} {1} نمی تواند بیشتر از کل کل {2} باشد, +Advance payments allocated against orders will only be fetched,پیش پرداخت های تخصیص یافته در برابر سفارش ها فقط واکشی می شود, +Affected Transactions,معاملات تحت تأثیر, +Against Customer Order {0},در مقابل سفارش مشتری {0}, +Against Supplier Invoice {0},در مقابل فاکتور تامین کننده {0}, +Against Voucher No,در مقابل کوپن شماره, +Age ({0}),سن ({0}), +Ageing Range,محدوده سالخوردگی, +Agent Busy Message,پیام مامور مشغول, +Agent Group,گروه عامل, +Agent Unavailable Message,پیام عامل در دسترس نیست, +Aggregate a group of Items into another Item. This is useful if you are maintaining the stock of the packed items and not the bundled item,گروهی از آیتم‌ها را در یک آیتم دیگر جمع کنید. اگر موجودی آیتم‌های بسته بندی شده را نگهداری می کنید و نه آیتم باندل شده، مفید است, +Algorithm,الگوریتم, +All Activities,تمام فعالیت ها, +All Activities HTML,تمام فعالیت ها HTML, +All Items,همه آیتم ها, +All allocations have been successfully reconciled,همه تخصیص ها با موفقیت تطبیق داده شده است, +All items have already been received,همه آیتم‌ها قبلاً دریافت شده است, +All items in this document already have a linked Quality Inspection.,همه آیتم‌ها در این سند قبلاً دارای یک بازرسی کیفیت مرتبط هستند., +All the Comments and Emails will be copied from one document to another newly created document(Lead -> Opportunity -> Quotation) throughout the CRM documents.,تمام نظرات و ایمیل ها از یک سند به سند جدید ایجاد شده دیگر (سرنخ -> فرصت -> پیش فاکتور) در سراسر اسناد CRM کپی می شوند., +"All the required items (raw materials) will be fetched from BOM and populated in this table. Here you can also change the Source Warehouse for any item. And during the production, you can track transferred raw materials from this table.",تمام آیتم‌های مورد نیاز (مواد اولیه) از BOM واکشی شده و در این جدول پر می شود. در اینجا شما همچنین می توانید انبار منبع را برای هر آیتم تغییر دهید. و در حین تولید می توانید مواد اولیه انتقال یافته را از این جدول ردیابی کنید., +Allocate Payment Request,تخصیص درخواست پرداخت, +Allocated Entries,ورودی های اختصاص داده شده, +Allocated To:,اختصاص داده شده به:, +Allocations,تخصیص ها, +Allow,اجازه, +Allow Alternative Item must be checked on Item {},آیتم جایگزین مجاز است باید برای آیتم {} علامت زده شود, +Allow Continuous Material Consumption,اجازه مصرف مداوم مواد, +Allow Excess Material Transfer,اجازه انتقال مواد اضافی را بدهید, +Allow Item to be Added Multiple Times in a Transaction,اجازه دهید آیتم چندین بار در یک تراکنش اضافه شود, +Allow Lead Duplication based on Emails,اجازه تکرار سرنخ بر اساس ایمیل, +Allow Negative rates for Items,نرخ های منفی برای آیتم‌ها مجاز است, +Allow Or Restrict Dimension,اجازه یا محدود کردن ابعاد, +Allow Partial Reservation,اجازه رزرو جزئی, +Allow Purchase,اجازه خرید, +Allow Sales,اجازه فروش, +Allow Sales Order Creation For Expired Quotation,اجازه ایجاد سفارش فروش برای قیمت منقضی شده, +Allow User to Edit Discount,به کاربر اجازه ویرایش تخفیف را بدهید, +Allow User to Edit Rate,به کاربر اجازه ویرایش نرخ بدهید, +Allow Zero Rate,اجازه نرخ صفر, +Allow material consumptions without immediately manufacturing finished goods against a Work Order,اجازه مصرف مواد را بدون تولید فوری کالاهای تمام شده در برابر دستور کار بدهید, +Allow multi-currency invoices against single party account , صورتحساب‌های چند ارزی را در برابر حساب یک طرف مجاز کنید, +Allow to Edit Stock UOM Qty for Purchase Documents,اجازه ویرایش موجودی UOM Qty برای اسناد خرید, +Allow to Edit Stock UOM Qty for Sales Documents,امکان ویرایش موجودی UOM Qty برای اسناد فروش, +Allow transferring raw materials even after the Required Quantity is fulfilled,امکان انتقال مواد خام حتی پس از برآورده شدن مقدار مورد نیاز, +Allowed,مجاز, +Allowed Dimension,ابعاد مجاز, +Allowed Doctypes,Doctypes مجاز, +Allowed Items,آیتم‌های مجاز, +Allowed primary roles are 'Customer' and 'Supplier'. Please select one of these roles only.,نقش‌های اصلی مجاز عبارتند از «مشتری» و «تامین‌کننده». لطفا فقط یکی از این نقش ها را انتخاب کنید., +Allows to keep aside a specific quantity of inventory for a particular order.,اجازه می دهد تا مقدار مشخصی از موجودی را برای یک سفارش خاص کنار بگذارید., +Already Picked,قبلاً انتخاب شده است, +Alternative Items,آیتم‌های جایگزین, +"Alternatively, you can download the template and fill your data in.",همچنین می توانید الگو را دانلود کرده و داده های خود را پر کنید., +Amount (AED),مقدار (AED), +Amount Eligible for Commission,مبلغ واجد شرایط برای کمیسیون, +Amount in Account Currency,مبلغ به ارز حساب, +Amount in party's bank account currency,مبلغ به ارز حساب بانکی طرف, +Amount in transaction currency,مبلغ به ارز تراکنش, +An Item Group is a way to classify items based on types.,گروه آیتم راهی برای دسته بندی آیتم‌ها بر اساس انواع است., +An error has been appeared while reposting item valuation via {0},هنگام ارسال مجدد ارزیابی مورد از طریق {0} خطایی ظاهر شد, +An error has occurred during {0}. Check {1} for more details,خطایی در طول {0} رخ داده است. برای جزئیات بیشتر {1} را بررسی کنید,Error Log +Annual Revenue,درآمد سالانه, +"Another Cost Center Allocation record {0} applicable from {1}, hence this allocation will be applicable upto {2}",یکی دیگر از رکوردهای تخصیص مرکز هزینه {0} قابل اعمال از {1}، بنابراین این تخصیص تا {2} قابل اعمال خواهد بود., +"Any one of following filters required: warehouse, Item Code, Item Group",هر یک از فیلترهای زیر مورد نیاز است: انبار، کد آیتم، گروه آیتم, +Applicable Dimension,ابعاد قابل اجرا, +Applicable On Account,قابل اجرا در حساب, +Applied on each reading.,در هر خواندن اعمال می شود., +Applied putaway rules.,اعمال قوانین حذف, +Apply Putaway Rule,اعمال قانون Putaway, +Apply Recursion Over (As Per Transaction UOM),اعمال بازگشت بیش از (بر اساس UOM تراکنش), +Apply SLA for Resolution Time,اعمال SLA برای زمان حل و فصل, +Apply TDS,TDS را اعمال کنید, +Apply Tax Withholding Amount , اعمال مبلغ کسر مالیات, +Apply restriction on dimension values,اعمال محدودیت در مقادیر ابعاد, +Apply to All Inventory Documents,برای همه اسناد موجودی اعمال شود, +Apply to Document,درخواست برای سند, +Appointment Created Successfully,قرار ملاقات با موفقیت ایجاد شد, +Appointment Scheduling Disabled,زمان‌بندی قرار غیرفعال است, +Appointment Scheduling has been disabled for this site,زمان‌بندی قرار برای این سایت غیرفعال شده است, +Appointment was created. But no lead was found. Please check the email to confirm,قرار ملاقات ایجاد شد. اما سرنخی پیدا نشد. لطفا برای تایید ایمیل را بررسی کنید, +Approximately match the description/party name against parties,تقریباً توصیف/نام طرف را با طرف‌ها مطابقت دهید, +Are you sure you want to clear all demo data?,آیا مطمئن هستید که می خواهید تمام داده های نمایشی را پاک کنید؟, +Are you sure you want to delete this Item?,آیا مطمئن هستید که میخواهید این آیتم را حذف کنید؟, +Are you sure you want to restart this subscription?,آیا مطمئن هستید که می خواهید این اشتراک را مجدداً راه‌اندازی کنید؟, +As on Date,همانطور که در تاریخ, +"As there are existing submitted transactions against item {0}, you can not change the value of {1}.",از آنجایی که تراکنش‌های ارسالی موجود در مقابل آیتم {0} وجود دارد، نمی‌توانید مقدار {1} را تغییر دهید., +"As there are negative stock, you can not enable {0}.",از آنجایی که موجودی منفی وجود دارد، نمی توانید {0} را فعال کنید., +"As there are reserved stock, you cannot disable {0}.",از آنجایی که موجودی رزرو شده وجود دارد، نمی توانید {0} را غیرفعال کنید., +"As there are sufficient Sub Assembly Items, Work Order is not required for Warehouse {0}.",از آنجایی که آیتم‌های زیر مونتاژ کافی وجود دارد، برای انبار {0} نیازی به دستور کار نیست., +"As {0} is enabled, you can not enable {1}.",از آنجایی که {0} فعال است، نمی توانید {1} را فعال کنید., +Assembly Items,آیتم‌های مونتاژ, +Asset Activity,فعالیت دارایی, +Asset Capitalization,سرمایه گذاری دارایی ها, +Asset Capitalization Asset Item,دارایی دارایی با سرمایه, +Asset Capitalization Service Item,آیتم خدمات سرمایه گذاری دارایی, +Asset Capitalization Stock Item,آیتم موجودی سرمایه گذاری دارایی, +Asset Depreciation Details,جزئیات استهلاک دارایی, +Asset Depreciation Schedule,جدول استهلاک دارایی ها, +Asset Depreciation Schedule for Asset {0} and Finance Book {1} is not using shift based depreciation,برنامه استهلاک دارایی برای دارایی {0} و کتاب مالی {1} از استهلاک مبتنی بر شیفت استفاده نمی کند, +Asset Depreciation Schedule not found for Asset {0} and Finance Book {1},برنامه استهلاک دارایی برای دارایی {0} و کتاب مالی {1} یافت نشد, +Asset Depreciation Schedule {0} for Asset {1} already exists.,برنامه استهلاک دارایی {0} برای دارایی {1} از قبل وجود دارد., +Asset Depreciation Schedule {0} for Asset {1} and Finance Book {2} already exists.,برنامه استهلاک دارایی {0} برای دارایی {1} و کتاب مالی {2} از قبل وجود دارد., +"Asset Depreciation Schedules created:
{0}

Please check, edit if needed, and submit the Asset.",برنامه‌های استهلاک دارایی ایجاد شده:
{0}

لطفاً بررسی کنید، در صورت نیاز ویرایش کنید و دارایی را ارسال کنید., +Asset ID,شناسه دارایی, +Asset Quantity,مقدار دارایی, +Asset Repair Consumed Item,کالای مصرفی تعمیر دارایی, +Asset Settings,تنظیمات دارایی, +Asset Shift Allocation,تخصیص تغییر دارایی, +Asset Shift Factor,عامل تغییر دارایی, +Asset Shift Factor {0} is set as default currently. Please change it first.,عامل تغییر دارایی {0} در حال حاضر به عنوان پیش فرض تنظیم شده است. لطفا ابتدا آن را تغییر دهید., +Asset cancelled,دارایی لغو شد, +Asset capitalized after Asset Capitalization {0} was submitted,پس از ارسال دارایی با حروف بزرگ {0} دارایی با حروف بزرگ نوشته شد, +Asset created,دارایی ایجاد شد, +Asset created after Asset Capitalization {0} was submitted,دارایی ایجاد شده پس از ارسال با حروف بزرگ دارایی {0}, +Asset created after being split from Asset {0},دارایی پس از جدا شدن از دارایی {0} ایجاد شد, +Asset decapitalized after Asset Capitalization {0} was submitted,پس از ارسال دارایی با سرمایه {0}، دارایی از سرمایه خارج شد, +Asset deleted,دارایی حذف شد, +Asset issued to Employee {0},دارایی برای کارمند {0} حواله شده, +Asset out of order due to Asset Repair {0},دارایی از کار افتاده به دلیل تعمیر دارایی {0}, +Asset received at Location {0} and issued to Employee {1},دارایی در مکان {0} دریافت و برای کارمند {1} حواله شد, +Asset restored,دارایی بازیابی شد, +Asset restored after Asset Capitalization {0} was cancelled,دارایی پس از لغو حروف بزرگ دارایی {0} بازیابی شد, +Asset returned,دارایی برگردانده شد, +Asset scrapped,دارایی از بین رفته است, +Asset sold,دارایی فروخته شده, +Asset submitted,دارایی ارسال شد, +Asset transferred to Location {0},دارایی به مکان {0} منتقل شد, +Asset updated after being split into Asset {0},دارایی پس از تقسیم به دارایی {0} به روز شد, +Asset updated after cancellation of Asset Repair {0},دارایی پس از لغو تعمیر دارایی {0} به روز شد, +Asset updated after completion of Asset Repair {0},دارایی پس از اتمام تعمیر دارایی به روز شد {0}, +Asset {0} cannot be received at a location and given to an employee in a single movement,دارایی {0} را نمی توان در یک مکان دریافت کرد و در یک حرکت به کارمند داد, +Asset {0} does not belong to Item {1},دارایی {0} به آیتم {1} تعلق ندارد, +Asset {0} does not exist,دارایی {0} وجود ندارد, +Asset {0} has been created. Please set the depreciation details if any and submit it.,دارایی {0} ایجاد شده است. لطفاً جزئیات استهلاک را در صورت وجود تنظیم و ارسال کنید., +Asset {0} has been updated. Please set the depreciation details if any and submit it.,دارایی {0} به روز شده است. لطفاً جزئیات استهلاک را در صورت وجود تنظیم و ارسال کنید., +Asset's depreciation schedule updated after Asset Shift Allocation {0},برنامه استهلاک دارایی پس از تخصیص تغییر دارایی {0} به روز شد, +Asset's value adjusted after cancellation of Asset Value Adjustment {0},ارزش دارایی پس از لغو تعدیل ارزش دارایی تنظیم شد {0}, +Asset's value adjusted after submission of Asset Value Adjustment {0},ارزش دارایی پس از ارسال تعدیل ارزش دارایی تنظیم شد {0}, +Assign Job to Employee,کار را به کارمند واگذار کنید, +Assignment,تخصیص, +Assignment Conditions,شرایط تخصیص, +At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} for the batch {4} in the warehouse {5}.,در ردیف #{0}: مقدار انتخاب شده {1} برای آیتم {2} بیشتر از موجودی در دسترس {3} برای دسته {4} در انبار {5} است., +At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} in the warehouse {4}.,در ردیف #{0}: مقدار انتخاب شده {1} برای آیتم {2} بیشتر از موجودی در دسترس {3} در انبار {4} است., +At row {0}: Batch No is mandatory for Item {1},در ردیف {0}: شماره دسته برای مورد {1} اجباری است, +At row {0}: Parent Row No cannot be set for item {1},در ردیف {0}: ردیف والد برای آیتم {1} قابل تنظیم نیست, +At row {0}: Qty is mandatory for the batch {1},در ردیف {0}: مقدار برای دسته {1} اجباری است, +At row {0}: Serial No is mandatory for Item {1},در ردیف {0}: شماره سریال برای آیتم {1} اجباری است, +At row {0}: Serial and Batch Bundle {1} has already created. Please remove the values from the serial no or batch no fields.,در ردیف {0}: باندل سریال و دسته {1} قبلا ایجاد شده است. لطفاً مقادیر را از فیلدهای شماره سریال یا شماره دسته حذف کنید., +At row {0}: set Parent Row No for item {1},در ردیف {0}: تنظیم شماره ردیف والد برای آیتم {1}, +Attach CSV File,پیوست فایل CSV, +Attendance & Leaves,حضور و غیاب و مرخصی, +Attribute value: {0} must appear only once,مقدار مشخصه: {0} باید فقط یک بار ظاهر شود, +Auto Create Exchange Rate Revaluation,ایجاد خودکار تجدید ارزیابی نرخ ارز, +Auto Create Purchase Receipt,ایجاد خودکار رسید خرید, +Auto Create Serial and Batch Bundle For Outward,ایجاد خودکار باندل سریال و دسته برای بیرون, +Auto Create Subcontracting Order,ایجاد خودکار سفارش قرارداد فرعی, +Auto Created Serial and Batch Bundle,باندل سریال و دسته ایجاد شده به صورت خودکار, +Auto Creation of Contact,ایجاد خودکار مخاطب, +Auto Email Report,گزارش خودکار ایمیل, +Auto Insert Item Price If Missing,درج خودکار قیمت آیتم در صورت فراموش شدن, +Auto Name,نام خودکار, +Auto Reconcile,تطبیق خودکار, +Auto Reconcile Payments,تطبیق خودکار پرداخت ها, +Auto Reconciliation,تطبیق خودکار, +Auto Reconciliation of Payments has been disabled. Enable it through {0},تطبیق خودکار پرداخت‌ها غیرفعال شده است. آن را از طریق {0} فعال کنید, +Auto Reserve Serial and Batch Nos,شماره سریال و دسته رزرو خودکار, +Auto Reserve Stock for Sales Order on Purchase,ذخیره خودکار موجودی برای سفارش فروش در هنگام خرید, +Auto close Opportunity Replied after the no. of days mentioned above,فرصت بسته شدن خودکار پس از خیر پاسخ داده شد. از روزهای ذکر شده در بالا, +Auto match and set the Party in Bank Transactions,مطابقت خودکار و تنظیم طرف در معاملات بانکی, +Auto write off precision loss while consolidation,حذف خودکار از دست دادن دقت در هنگام تلفیق, +Automatically Add Filtered Item To Cart,افزودن خودکار آیتم فیلتر شده به سبد خرید, +Automatically Fetch Payment Terms from Order,واکشی خودکار شرایط پرداخت از سفارش, +Automatically post balancing accounting entry,ثبت حسابداری تراز به طور خودکار, +Available Qty For Consumption,تعداد موجود برای مصرف, +Available Qty at Company,تعداد موجود در شرکت, +Available Qty at Target Warehouse,تعداد موجود در انبار هدف, +Available Qty to Reserve,تعداد برای رزرو موجود است, +Average Completion,میانگین تکمیل, +Avg Rate,میانگین نرخ, +Avg Rate (Balance Stock),میانگین نرخ (تراز موجودی), +BOM Created,BOM ایجاد شد, +BOM Creator,ایجاد کننده BOM, +BOM Creator Item,آیتم ایجاد کننده BOM, +BOM Info,اطلاعات BOM, +BOM Level,سطح BOM, +BOM Tree,درخت BOM, +BOM UoM,واحد اندازه گیری BOM, +BOM Update Batch,دسته به روز رسانی BOM, +BOM Update Initiated,به روز رسانی BOM آغاز شد, +BOM Update Log,لاگ به روز رسانی BOM, +BOM Updation already in progress. Please wait until {0} is complete.,به‌روزرسانی BOM در حال انجام است. لطفاً صبر کنید تا {0} کامل شود., +BOM Updation is queued and may take a few minutes. Check {0} for progress.,به روز رسانی BOM در صف است و ممکن است چند دقیقه طول بکشد. برای پیشرفت، {0} را بررسی کنید., +BOM and Production,BOM و تولید, +BOM recursion: {1} cannot be parent or child of {0},بازگشت BOM: {1} نمی تواند والد یا فرزند {0} باشد, +BOMs Updated,BOM ها به روز شدند, +BOMs created successfully,BOM با موفقیت ایجاد شد, +BOMs creation failed,ایجاد BOM ناموفق بود, +"BOMs creation has been enqueued, kindly check the status after some time",ایجاد BOM در نوبت قرار گرفته است، لطفاً وضعیت را پس از مدتی بررسی کنید, +Balance Qty (Stock),مقدار تراز (موجودی), +Balance Sheet Summary,خلاصه ترازنامه, +Balance Stock Value,تراز ارزش موجودی, +Bank Reconciliation Tool,ابزار تطبیق بانکی, +Bank Statement Import,درون‌بُرد صورتحساب بانکی, +Bank Transaction {0} Matched,تراکنش بانکی {0} مطابقت دارد, +Bank Transaction {0} added as Journal Entry,تراکنش بانکی {0} به عنوان ثبت دفتر روزنامه اضافه شد, +Bank Transaction {0} added as Payment Entry,تراکنش بانکی {0} به عنوان ثبت پرداخت اضافه شد, +Bank Transaction {0} is already fully reconciled,تراکنش بانکی {0} در حال حاضر به طور کامل تطبیق شده است, +Bank Transaction {0} updated,تراکنش بانکی {0} به روز شد, +Bank/Cash Account,بانک / حساب نقدی, +Bank/Cash Account {0} doesn't belong to company {1},حساب بانکی/نقدی {0} به شرکت {1} تعلق ندارد, +Base Amount,مقدار پایه, +Base Cost Per Unit,هزینه پایه به ازای هر واحد, +Base Rate,نرخ پایه, +Base Tax Withholding Net Total,کل خالص کسر مالیات پایه, +Base Total,مجموع پایه, +Base Total Billable Amount,مبنا کل مبلغ قابل پرداخت, +Base Total Billed Amount,مبنا کل مبلغ صورتحساب, +Base Total Costing Amount,مبنا کل بهای تمام شده, +Based On Value,بر اساس ارزش, +"Based on your HR Policy, select your leave allocation period's end date",بر اساس خط مشی منابع انسانی خود، تاریخ پایان دوره تخصیص مرخصی خود را انتخاب کنید, +"Based on your HR Policy, select your leave allocation period's start date",بر اساس خط مشی منابع انسانی خود، تاریخ شروع دوره تخصیص مرخصی خود را انتخاب کنید, +Batch No is mandatory,شماره دسته اجباری است, +Batch No {0} does not exists,شماره دسته {0} وجود ندارد, +Batch No {0} is linked with Item {1} which has serial no. Please scan serial no instead.,شماره دسته {0} با آیتم {1} که دارای شماره سریال است پیوند داده شده است. لطفاً شماره سریال را اسکن کنید., +Batch No.,شماره دسته, +Batch Nos,شماره های دسته, +Batch Nos are created successfully,شماره های دسته با موفقیت ایجاد شد, +Batch Qty,مقدار دسته, +Batch and Serial No,شماره دسته و سریال, +Batch not created for item {} since it does not have a batch series.,دسته ای برای آیتم {} ایجاد نشده است زیرا سری دسته ای ندارد., +Batch {0} and Warehouse,دسته {0} و انبار, +Batch {0} is not available in warehouse {1},دسته {0} در انبار {1} موجود نیست, +Batchwise Valuation,ارزش گذاری دسته ای, +Beginning of the current subscription period,شروع دوره اشتراک فعلی, +Bill for Rejected Quantity in Purchase Invoice,صورتحساب مقدار رد شده در فاکتور خرید, +Billed Items To Be Received,آیتم‌های صورتحساب شده برای دریافت, +"Billed, Received & Returned",صورتحساب، دریافت و برگردانده شد, +Billing Address Details,جزئیات آدرس صورتحساب, +Billing Interval in Subscription Plan must be Month to follow calendar months,فاصله صورتحساب در طرح اشتراک باید ماه باشد تا ماه‌های تقویمی را دنبال کنید, +Blanket Order Allowance (%),سفارش کلی مجاز (%), +Bom No,شماره BOM, +Book Advance Payments as Liability option is chosen. Paid From account changed from {0} to {1}.,گزینه رزرو پیش پرداخت به عنوان بدهی انتخاب شده است. حساب Paid From از {0} به {1} تغییر کرد., +Book Advance Payments in Separate Party Account,پیش پرداخت را در حساب طرف جداگانه رزرو کنید, +Book Tax Loss on Early Payment Discount,از دست دادن مالیات در تخفیف پرداخت زودهنگام رزرو کنید, +Book an appointment,یک قرار ملاقات رزرو کنید, +Booking stock value across multiple accounts will make it harder to track stock and account value.,رزرو ارزش موجودی در چندین حساب، ردیابی موجودی و ارزش حساب را دشوارتر می کند., +Books have been closed till the period ending on {0},کتاب‌ها تا پایان دوره {0} بسته شده‌اند, +Budget Exceeded,بودجه بیش از حد, +Build All?,ساخت همه؟, +Buildable Qty,تعداد قابل ساخت, +Bulk Transaction Log,لاگ تراکنش های انبوه, +Bulk Transaction Log Detail,جزئیات لاگ تراکنش های انبوه, +Bulk Update,به روز رسانی انبوه, +Bundle Items,آیتم‌های باندل, +Buying & Selling Settings,تنظیمات خرید و فروش, +Buying and Selling,خرید و فروش, +"By default, the Supplier Name is set as per the Supplier Name entered. If you want Suppliers to be named by a Naming Series choose the 'Naming Series' option.","به‌طور پیش‌فرض، نام تامین‌کننده مطابق با نام تامین‌کننده وارد شده تنظیم می‌شود. اگر می‌خواهید تامین‌کنندگان با سری نام‌گذاری نام‌گذاری شوند. گزینه ""Naming Series"" را انتخاب کنید.", +Bypass credit check at Sales Order,دور زدن بررسی اعتبار در سفارش فروش, +COGS By Item Group,COGS بر اساس گروه آیتم, +CRM Note,یادداشت CRM, +CRM Settings,تنظیمات CRM, +Calculate Product Bundle Price based on Child Items' Rates,قیمت باندل محصول را بر اساس نرخ آیتم‌های فرزند محاسبه کنید, +Call Again,دوباره تماس بگیر, +Call Ended,مکالمه تلفنی تمام شد, +Call Handling Schedule,برنامه رسیدگی به تماس ها, +Call Received By,تماس دریافت شده توسط, +Call Receiving Device,دستگاه دریافت تماس, +Call Routing,مسیریابی تماس, +Call Schedule Row {0}: To time slot should always be ahead of From time slot.,ردیف زمان‌بندی تماس {0}: بازه زمانی To همیشه باید جلوتر از بازه زمانی از زمان باشد., +Call Type,نوع تماس, +Callback,پاسخ به تماس, +Campaign Item,آیتم کمپین, +Can not close Work Order. Since {0} Job Cards are in Work In Progress state.,نمی توان دستور کار را بست. از آنجایی که کارت کارهای {0} در حالت کار در حال انجام هستند., +"Can not filter based on Child Account, if grouped by Account",اگر براساس حساب گروه‌بندی شود، نمی‌توان بر اساس حساب فرزند فیلتر کرد, +"Can't change the valuation method, as there are transactions against some items which do not have its own valuation method",نمی توان روش ارزش گذاری را تغییر داد، زیرا معاملاتی در برابر برخی اقلام وجود دارد که روش ارزش گذاری خاص خود را ندارند., +Cannot Merge,نمی توان ادغام کرد, +Cannot Resubmit Ledger entries for vouchers in Closed fiscal year.,نمی‌توان ورودی‌های دفتر کل را برای کوپن‌ها در سال مالی بسته دوباره ارسال کرد., +"Cannot amend {0} {1}, please create a new one instead.",نمی توان {0} {1} را اصلاح کرد، لطفاً در عوض یک مورد جدید ایجاد کنید., +Cannot apply TDS against multiple parties in one entry,نمی‌توان TDS را در یک ورودی در مقابل چندین طرف اعمال کرد, +Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet.,نمی توان تراکنش را لغو کرد. ارسال مجدد ارزیابی اقلام هنگام ارسال هنوز تکمیل نشده است., +Cannot change Reference Document Type.,نمی توان نوع سند مرجع را تغییر داد., +Cannot complete task {0} as its dependant task {1} are not completed / cancelled.,نمی توان کار {0} را تکمیل کرد زیرا وظیفه وابسته آن {1} تکمیل نشده / لغو شد., +Cannot convert Task to non-group because the following child Tasks exist: {0}.,نمی توان وظیفه را به غیر گروهی تبدیل کرد زیرا وظایف فرزند زیر وجود دارد: {0}., +Cannot convert to Group because Account Type is selected.,نمی توان به گروه تبدیل کرد زیرا نوع حساب انتخاب شده است., +Cannot create Stock Reservation Entries for future dated Purchase Receipts.,نمی توان ورودی های رزرو موجودی را برای رسیدهای خرید با تاریخ آینده ایجاد کرد., +Cannot create a pick list for Sales Order {0} because it has reserved stock. Please unreserve the stock in order to create a pick list.,نمی‌توان فهرست انتخابی برای سفارش فروش {0} ایجاد کرد زیرا موجودی رزرو کرده است. لطفاً برای ایجاد لیست انتخاب، موجودی را لغو رزرو کنید., +Cannot create accounting entries against disabled accounts: {0},نمی توان ورودی های حسابداری را در برابر حساب های غیرفعال ایجاد کرد: {0}, +Cannot enqueue multi docs for one company. {0} is already queued/running for company: {1},نمی توان چند سند را برای یک شرکت در صف قرار داد. {0} قبلاً برای شرکت: {1} در صف/در حال اجراست, +Cannot find a default warehouse for item {0}. Please set one in the Item Master or in Stock Settings.,نمی توان یک انبار پیش فرض برای آیتم {0} پیدا کرد. لطفاً یکی را در مدیریت آیتم یا در تنظیمات سهام تنظیم کنید., +Cannot make any transactions until the deletion job is completed,تا زمانی که کار حذف کامل نشود، نمی توان هیچ تراکنشی انجام داد, +Cannot produce more item for {0},نمی توان مورد بیشتری برای {0} تولید کرد, +Cannot produce more than {0} items for {1},نمی توان بیش از {0} مورد برای {1} تولید کرد, +Cannot receive from customer against negative outstanding,نمی توان از مشتری در برابر معوقات منفی دریافت کرد, +Cannot retrieve link token for update. Check Error Log for more information,نمی توان توکن پیوند را برای به روز رسانی بازیابی کرد. برای اطلاعات بیشتر Log خطا را بررسی کنید, +Cannot retrieve link token. Check Error Log for more information,توکن پیوند بازیابی نمی شود. برای اطلاعات بیشتر Log خطا را بررسی کنید, +Capacity (Stock UOM),ظرفیت (Stock UOM), +Capacity in Stock UOM,ظرفیت موجود در انبار UOM, +Capacity must be greater than 0,ظرفیت باید بیشتر از 0 باشد, +Capitalization,حروف بزرگ, +Capitalization Method,روش حروف بزرگ, +Capitalize Asset,سرمایه گذاری دارایی, +Capitalize Repair Cost,سرمایه گذاری در هزینه تعمیر, +Capitalized,با حروف بزرگ, +Carrier,حامل, +Carrier Service,خدمات حامل, +Carry Forward Communication and Comments,انتقال ارتباطات و نظرات, +Category Details,جزئیات دسته, +Caution: This might alter frozen accounts.,احتیاط: این ممکن است حساب های مسدود شده را تغییر دهد., +Change in Stock Value,تغییر در ارزش موجودی, +Changed customer name to '{}' as '{}' already exists.,"نام مشتری به ""{}"" به عنوان ""{}"" تغییر کرده است.", +Changes,تغییرات, +Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount,"هزینه از نوع ""واقعی"" در ردیف {0} نمی تواند در نرخ مورد یا مبلغ پرداختی لحاظ شود", +Chart Of Accounts,نمودار حساب, +Checking this will round off the tax amount to the nearest integer,بررسی این مقدار مالیات را به نزدیکترین عدد صحیح گرد می کند, +Choose a WIP composite asset,یک دارایی ترکیبی «کار در حال انجام» را انتخاب کنید, +Clear Demo Data,پاک کردن داده های نمایشی, +Clear Notifications,پاک کردن اعلان ها, +Clearing Demo Data...,در حال پاک کردن داده های نمایشی..., +Click on 'Get Finished Goods for Manufacture' to fetch the items from the above Sales Orders. Items only for which a BOM is present will be fetched.,"برای دریافت آیتم‌ها از سفارش‌های فروش فوق، روی ""دریافت کالاهای تمام شده برای ساخت"" کلیک کنید. فقط آیتم‌هایی که BOM برای آنها وجود دارد واکشی می شوند.", +Click on Add to Holidays. This will populate the holidays table with all the dates that fall on the selected weekly off. Repeat the process for populating the dates for all your weekly holidays,روی افزودن به تعطیلات کلیک کنید. با این کار جدول تعطیلات با تمام تاریخ هایی که در تعطیلات هفتگی انتخاب شده قرار می گیرند پر می کند. فرآیند پر کردن تاریخ ها را برای تمام تعطیلات هفتگی خود تکرار کنید, +Click on Get Sales Orders to fetch sales orders based on the above filters.,برای دریافت سفارش‌های فروش بر اساس فیلترهای بالا، روی دریافت سفارش‌های فروش کلیک کنید., +Click to add email / phone,برای افزودن ایمیل / تلفن کلیک کنید, +Close Replied Opportunity After Days,بستن فرصت پاسخ داده شده پس از چند روز, +Closed Work Order can not be stopped or Re-opened,دستور کار بسته را نمی توان متوقف کرد یا دوباره باز کرد, +Closing,بسته شدن, +Closing Balance as per Bank Statement,مانده پایانی طبق صورتحساب بانکی, +Closing Balance as per ERP,تراز پایانی طبق ERP, +Closing Stock Balance,تراز پایانی موجودی, +Communication Channel,کانال ارتباطی, +Company Address Display,نمایش آدرس شرکت, +Company Billing Address,آدرس صورتحساب شرکت, +Company Details,جزئیات شرکت, +Company Shipping Address,آدرس حمل و نقل شرکت, +Company Tax ID,شناسه مالیاتی شرکت, +Company and Posting Date is mandatory,شرکت و تاریخ ارسال الزامی است, +Company is mandatory,شرکت الزامی است, +Company is mandatory for generating an invoice. Please set a default company in Global Defaults.,شرکت برای تهیه فاکتور الزامی است. لطفاً یک شرکت پیش‌فرض را در پیش‌فرض‌های سراسری تنظیم کنید., +Company which internal customer represents,شرکتی که مشتری داخلی نماینده آن است, +Company which internal customer represents.,شرکتی که مشتری داخلی نماینده آن است., +Company which internal supplier represents,شرکتی که تامین کننده داخلی آن را نمایندگی می کند, +Company {0} is added more than once,شرکت {0} بیش از یک بار اضافه شده است, +Company {} does not exist yet. Taxes setup aborted.,شرکت {} هنوز وجود ندارد. تنظیم مالیات لغو شد., +Company {} does not match with POS Profile Company {},شرکت {} با نمایه POS شرکت {} مطابقت ندارد, +Competitor,رقیب, +Competitor Detail,جزئیات رقیب, +Competitor Name,نام رقیب, +Competitors,رقبا, +Complete Job,کار کامل, +Completed On,تکمیل شد, +Completed On cannot be greater than Today,تکمیل شده در تاریخ نمی تواند بزرگتر از امروز باشد, +Completed Tasks,وظایف تکمیل شده, +Completed Time,زمان تکمیل شده, +Conditional Rule,قانون مشروط, +Conditional Rule Examples,مثال های قانون شرطی, +Configure Product Assembly,پیکربندی اسمبلی محصول, +Configure the action to stop the transaction or just warn if the same rate is not maintained.,کنش را طوری پیکربندی کنید که تراکنش را متوقف کند یا در صورت عدم حفظ همان نرخ فقط هشدار دهد., +Connections,اتصالات, +Consider Entire Party Ledger Amount,کل مبلغ دفتر کل طرف را در نظر بگیرید, +Consider Minimum Order Qty,در نظر گرفتن حداقل تعداد سفارش, +Consider Rejected Warehouses,در نظر گرفتن انبارهای مرجوعی, +Considered In Paid Amount,به مبلغ پرداختی در نظر گرفته شده است, +Consolidate Sales Order Items,تلفیق آیتم‌های سفارش فروش, +Consolidate Sub Assembly Items,تلفیق آیتم‌های زیر مونتاز, +Consumed Asset Items is mandatory for Decapitalization,اقلام دارایی مصرف شده برای کاهش سرمایه اجباری است, +Consumed Asset Total Value,ارزش کل دارایی مصرف شده, +Consumed Assets,دارایی های مصرف شده, +Consumed Quantity,مقدار مصرف شده, +Consumed Stock Items,آیتم‌های موجودی مصرفی, +Consumed Stock Total Value,ارزش کل موجودی مصرف شده, +Consumption Rate,نرخ مصرف, +Contact Details,اطلاعات تماس, +Contact Mobile,موبایل مخاطب, +Contact Us Settings,تنظیمات تماس با ما, +Contacts,مخاطب, +Contract Template Help,راهنمای الگوی قرارداد, +Contribution Qty,مقدار مشارکت, +Control Historical Stock Transactions,معاملات تاریخی موجودی را کنترل کنید, +Convert Item Description to Clean HTML in Transactions,توضیحات مورد را به Clean HTML در Transactions تبدیل کنید, +Convert to Group,تبدیل به گروه,Warehouse +Convert to Item Based Reposting,تبدیل به ارسال مجدد بر اساس آیتم, +Convert to Ledger,تبدیل به دفتر کل,Warehouse +Core,هسته, +Corrective Job Card,کارت کار اصلاحی, +Corrective Operation,عملیات اصلاحی, +Corrective Operation Cost,هزینه عملیات اصلاحی, +Cost Center Allocation,تخصیص مرکز هزینه, +Cost Center Allocation Percentage,درصد تخصیص مرکز هزینه, +Cost Center Allocation Percentages,درصدهای تخصیص مرکز هزینه, +Cost Center For Item with Item Code {0} has been Changed to {1},مرکز هزینه برای مورد با کد مورد {0} به {1} تغییر کرده است, +"Cost Center is a part of Cost Center Allocation, hence cannot be converted to a group",مرکز هزینه بخشی از تخصیص مرکز هزینه است، بنابراین نمی توان آن را به یک گروه تبدیل کرد, +Cost Center with Allocation records can not be converted to a group,مرکز هزینه با رکوردهای تخصیص را نمی توان به گروه تبدیل کرد, +Cost Center {0} cannot be used for allocation as it is used as main cost center in other allocation record.,مرکز هزینه {0} را نمی توان برای تخصیص استفاده کرد زیرا به عنوان مرکز هزینه اصلی در سایر رکوردهای تخصیص استفاده می شود., +Cost Center {} doesn't belong to Company {},مرکز هزینه {} متعلق به شرکت {} نیست, +Cost Center {} is a group cost center and group cost centers cannot be used in transactions,مرکز هزینه {} یک مرکز هزینه گروهی است و مراکز هزینه گروهی را نمی توان در تراکنش ها استفاده کرد, +Cost Configuration,پیکربندی هزینه, +Cost Per Unit,هزینه هر واحد, +Cost of Poor Quality Report,گزارش هزینه کیفیت پایین, +Cost to Company (CTC),هزینه شرکت (CTC), +Costing Details,جزئیات هزینه, +Could Not Delete Demo Data,داده های نسخه ی نمایشی حذف نشد, +Could not auto update shifts. Shift with shift factor {0} needed.,به‌روزرسانی خودکار شیفت‌ها ممکن نیست. Shift با ضریب تغییر {0} مورد نیاز است., +Could not detect the Company for updating Bank Accounts,شرکت برای به‌روزرسانی حساب‌های بانکی شناسایی نشد, +Could not find path for , مسیری برای پیدا نشد, +Count,شمردن, +Create Depreciation Entry,ایجاد ثبت استهلاک, +Create Employee records.,ایجاد رکوردهای کارمندان., +Create Grouped Asset,ایجاد دارایی گروهی, +Create Job Card based on Batch Size,ایجاد کارت کار بر اساس اندازه دسته ای, +Create Ledger Entries for Change Amount,برای تغییر مقدار، ورودی های دفتر کل ایجاد کنید, +Create Link,ایجاد لینک, +Create Multi-level BOM,BOM چند سطحی ایجاد کنید, +Create New Customer,مشتری جدید ایجاد کنید, +Create Opportunity,ایجاد فرصت, +Create Prospect,ایجاد مشتری بالقوه, +Create Reposting Entries,ورودی های ارسال مجدد ایجاد کنید, +Create Reposting Entry,ایجاد ورودی ارسال مجدد, +Create Stock Entry,ایجاد ثبت موجودی, +Create a new composite asset,یک دارایی ترکیبی جدید ایجاد کنید, +Create {0} {1} ?,{0} {1} ایجاد شود؟, +Created On,ایجاد شد, +Created {0} scorecards for {1} between:,ایجاد {0} کارت امتیازی برای {1} بین:, +Creating Delivery Note ...,ایجاد یادداشت تحویل ..., +Creating Packing Slip ...,ایجاد برگه بسته بندی ..., +Creating Purchase Invoices ...,ایجاد فاکتورهای خرید ..., +Creating Purchase Receipt ...,ایجاد رسید خرید ..., +Creating Sales Invoices ...,ایجاد فاکتورهای فروش ..., +Creating Stock Entry,ایجاد ثبت موجودی, +Creating Subcontracting Order ...,ایجاد سفارش پیمانکاری فرعی ..., +Creating Subcontracting Receipt ...,ایجاد رسید پیمانکاری فرعی ..., +Creating User...,ایجاد کاربر..., +Creation,ایجاد, +Creation of {1}(s) successful,ایجاد {1}(ها) با موفقیت, +Credit (Transaction),اعتبار (تراکنش), +Credit Amount in Transaction Currency,مقدار اعتبار به ارز تراکنش, +Credit Limit Crossed,از حد اعتبار عبور کرد, +Credit Limit Settings,تنظیمات محدودیت اعتباری, +Currency Exchange Settings Details,جزئیات تنظیمات تبادل ارز, +Currency Exchange Settings Result,نتیجه تنظیمات تبادل ارز, +Current Asset,دارایی جاری, +Current Index,شاخص فعلی, +Current Level,سطح فعلی, +Current Liability,بدهی جاری, +Current Serial / Batch Bundle,باندل سریال / دسته فعلی, +Custom,سفارشی, +Customer , مشتری, +Customer / Item / Item Group,مشتری / آیتم / گروه آیتم, +Customer Defaults,پیش فرض های مشتری, +Customer Group Item,مورد گروه مشتری, +Customer Group: {0} does not exist,گروه مشتری: {0} وجود ندارد, +Customer Item,مورد مشتری, +Customer Name: , نام مشتری:, +Customer Portal Users,کاربران پورتال مشتری, +Customer: , مشتری:, +D - E,د - ای, +Daily Time to send,زمان ارسال روزانه, +Dashboard,داشبورد, +Data Based On,داده ها بر اساس, +Date , تاریخ, +Date must be between {0} and {1},تاریخ باید بین {0} و {1} باشد, +Days before the current subscription period,چند روز قبل از دوره اشتراک فعلی, +Deal Owner,صاحب معامله, +Debit (Transaction),بدهی (تراکنش), +Debit Amount in Transaction Currency,مبلغ بدهی به ارز تراکنش, +Decapitalization,سرمایه زدایی, +Decapitalized,بدون سرمایه, +Default Advance Account,حساب پیش فرض پیش فرض, +Default Advance Paid Account,حساب پیش فرض پیش پرداخت, +Default Advance Received Account,پیش فرض پیش فرض حساب دریافت شده, +Default BOM not found for FG Item {0},BOM پیش فرض برای آیتم کالای تمام شده {0} یافت نشد, +Default Discount Account,حساب تخفیف پیش فرض, +Default In-Transit Warehouse,انبار پیش فرض در حمل و نقل, +Default Payment Discount Account,حساب تخفیف پیش‌فرض پرداخت, +Default Provisional Account,حساب موقت پیش فرض, +Default Service Level Agreement for {0} already exists.,توافقنامه سطح سرویس پیش فرض برای {0} از قبل وجود دارد., +Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You need to either cancel the linked documents or create a new Item.,واحد اندازه گیری پیش فرض برای مورد {0} را نمی توان مستقیماً تغییر داد زیرا قبلاً تراکنش(هایی) را با UOM دیگری انجام داده اید. شما باید اسناد پیوند داده شده را لغو کنید یا یک مورد جدید ایجاد کنید., +Default settings for your stock-related transactions,تنظیمات پیش‌فرض برای تراکنش‌های مربوط به موجودی شما, +"Default tax templates for sales, purchase and items are created.",الگوهای مالیاتی پیش فرض برای فروش، خرید و اقلام ایجاد می شود., +Deferred Accounting,حسابداری معوق, +Deferred Accounting Defaults,پیش‌فرض‌های حسابداری معوق, +Deferred Revenue and Expense,درآمد و هزینه معوق, +Deferred accounting failed for some invoices:,حسابداری معوق برای برخی از فاکتورها ناموفق بود:, +Delay (In Days),تاخیر (در چند روز), +Delayed,با تاخیر, +Delayed Tasks Summary,خلاصه وظایف تاخیری, +Delete Accounting and Stock Ledger Entries on deletion of Transaction,حذف ورودی های حسابداری و دفتر کل موجودی در حذف تراکنش, +Delete Cancelled Ledger Entries,ورودی های لغو شده در دفتر کل را حذف کنید, +Delete Dimension,حذف ابعاد, +Delete Leads and Addresses,سرنخ ها و آدرس ها را حذف کنید, +Delete Transactions,حذف تراکنش ها, +Deleted Documents,اسناد حذف شده, +Deletion in Progress!,حذف در حال انجام است!, +Delimiter options,گزینه های جداکننده, +Delivery Manager,مدیر تحویل, +Delivery Note Packed Item,کالای بسته بندی شده یادداشت تحویل, +Delivery Note(s) created for the Pick List,یادداشت(های) تحویل برای لیست انتخاب ایجاد شده است, +Delivery User,کاربر تحویل, +Delivery to,تحویل به, +Demo Company,شرکت دمو, +Demo data cleared,داده‌های نمایشی پاک شد, +Dependant SLE Voucher Detail No,شماره جزئیات کوپن SLE وابسته, +Dependent Task {0} is not a Template Task,وظیفه وابسته {0} یک کار الگو نیست, +Deposit,سپرده, +Depreciate based on daily pro-rata,استهلاک بر اساس تناسب روزانه, +Depreciate based on shifts,استهلاک بر اساس نوبت, +Depreciation Details,جزئیات استهلاک, +Depreciation Entry Posting Status,وضعیت ثبت استهلاک, +Depreciation Expense Account should be an Income or Expense Account.,حساب هزینه استهلاک باید یک حساب درآمد یا هزینه باشد., +Depreciation Schedule View,مشاهده برنامه زمانبندی استهلاک, +Depreciation cannot be calculated for fully depreciated assets,استهلاک برای دارایی های کاملا مستهلک شده قابل محاسبه نیست, +Description of Content,شرح مطالب, +Desk User,کاربر میز, +Difference In,تفاوت در, +Difference Posting Date,تفاوت تاریخ ارسال, +Difference Qty,تفاوت تعداد, +Different 'Source Warehouse' and 'Target Warehouse' can be set for each row.,برای هر ردیف می توان «انبار منبع» و «انبار هدف» متفاوتی را تنظیم کرد., +Dimension Details,جزئیات ابعاد, +Dimension Filter Help,راهنمای فیلتر ابعاد, +Dimension-wise Accounts Balance Report,گزارش تراز حسابها از نظر ابعاد, +Direct Expense,هزینه مستقیم, +Disable Last Purchase Rate,نرخ آخرین خرید را غیرفعال کنید, +Disable Serial No And Batch Selector,غیرفعال کردن سریال No And Batch Selector, +Disabled Account Selected,حساب غیرفعال انتخاب شد, +Disabled Warehouse {0} cannot be used for this transaction.,از انبار غیرفعال شده {0} نمی توان برای این تراکنش استفاده کرد., +Disabled pricing rules since this {} is an internal transfer,قوانین قیمت گذاری غیرفعال شده است زیرا این {} یک انتقال داخلی است, +Disabled tax included prices since this {} is an internal transfer,مالیات غیرفعال شامل قیمت‌ها می‌شود زیرا این {} یک انتقال داخلی است, +Disables auto-fetching of existing quantity,واکشی خودکار مقدار موجود را غیرفعال می کند, +Disassemble,مجزا کردن (دیس اسمبل), +Discount Account,حساب تخفیف, +Discount Date,تاریخ تخفیف, +Discount Settings,تنظیمات تخفیف, +Discount Validity,اعتبار تخفیف, +Discount Validity Based On,اعتبار تخفیف بر اساس, +Discount of {} applied as per Payment Term,تخفیف {} طبق شرایط پرداخت اعمال شد, +Discounted Amount,مبلغ با تخفیف, +"Discounts to be applied in sequential ranges like buy 1 get 1, buy 2 get 2, buy 3 get 3 and so on",تخفیف هایی که در محدوده های متوالی اعمال می شوند مانند خرید 1 دریافت 1، خرید 2 دریافت 2، خرید 3 دریافت 3 و غیره, +Discrepancy between General and Payment Ledger,اختلاف بین دفتر کل و دفتر پرداخت, +Dispatch Address,آدرس اعزام, +Dispatch Address Name,نام آدرس اعزام, +Distinct Item and Warehouse,کالا و انبار متمایز, +Distribute Additional Costs Based On , توزیع هزینه های اضافی بر اساس, +Distribute Manually,توزیع دستی, +Do Not Explode,گسترده نکنید, +DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it.,DocTypes نباید به صورت دستی به جدول 'Excluded DocTypes' اضافه شود. شما فقط مجاز به حذف ورودی ها از آن هستید., +Document Type already used as a dimension,نوع سند قبلاً به عنوان بعد استفاده شده است, +Documents,اسناد, +Documents: {0} have deferred revenue/expense enabled for them. Cannot repost.,اسناد: {0} درآمد/هزینه معوق را برای آنها فعال کرده است. امکان ارسال مجدد وجود ندارد., +Domain Settings,تنظیمات دامنه, +Don't Reserve Sales Order Qty on Sales Return,تعداد سفارش فروش را در بازگشت فروش رزرو نکنید, +Don't Send Emails,ایمیل ارسال نکنید, +Dont Recompute tax,مالیات را دوباره محاسبه نکنید, +Download Backups,دانلود نسخه پشتیبان, +Download CSV Template,دانلود قالب CSV, +Download Materials Request Plan,دانلود طرح درخواست مواد, +Download Materials Request Plan Section,دانلود بخش طرح درخواست مواد, +Dunning Amount (Company Currency),مقدار Dunning (ارز شرکت), +Dunning Level,سطح دانینگ, +Duplicate Closing Stock Balance,تکرار کردن تراز موجودی اختتامیه, +Duplicate Customer Group,گروه مشتریان تکراری, +Duplicate Finance Book,کتاب مالی تکراری, +Duplicate Item Group,گروه آیتم تکراری, +Duplicate POS Invoices found,فاکتورهای POS تکراری پیدا شد, +Edit Capacity,ویرایش ظرفیت, +Edit Full Form,ویرایش فرم کامل, +Edit Note,ویرایش یادداشت, +Editing {0} is not allowed as per POS Profile settings,ویرایش {0} طبق تنظیمات نمایه POS مجاز نیست, +Either 'Selling' or 'Buying' must be selected,«فروش» یا «خرید» باید انتخاب شود, +Email / Notifications,ایمیل / اعلان ها, +Email Address (required),آدرس ایمیل (الزامی), +"Email Address must be unique, it is already used in {0}",آدرس ایمیل باید منحصر به فرد باشد، از قبل در {0} استفاده شده است, +Email Digest Recipient,دریافت کننده خلاصه ایمیل, +Email Digest: {0},خلاصه ایمیل: {0}, +Email Domain,دامنه ایمیل, +Email or Phone/Mobile of the Contact are mandatory to continue.,برای ادامه ایمیل یا تلفن/موبایل مخاطب الزامی است., +Email verification failed.,تأیید ایمیل انجام نشد., +Employee User Id,شناسه کاربر کارمند, +Enable Allow Partial Reservation in the Stock Settings to reserve partial stock.,برای رزرو موجودی جزئی، Allow Partial Reservation را در تنظیمات موجودی فعال کنید., +Enable Automatic Party Matching,تنظیم خودکار طرف را فعال کنید, +Enable Common Party Accounting,حسابداری طرف مشترک را فعال کنید, +Enable Discount Accounting for Selling,فعال کردن حسابداری تخفیف برای فروش, +Enable Fuzzy Matching,Fuzzy Matching را فعال کنید, +Enable Provisional Accounting For Non Stock Items,فعال کردن حسابداری موقت برای آیتم‌ها غیر موجودی, +Enable Stock Reservation,فعال کردن رزرو موجودی, +Enable it if users want to consider rejected materials to dispatch.,اگر کاربران می خواهند مواد رد شده را برای ارسال در نظر بگیرند، آن را فعال کنید., +Enable to apply SLA on every {0},فعال کردن اعمال SLA در هر {0}, +Enabling this option will allow you to record -

1. Advances Received in a Liability Account instead of the Asset Account

2. Advances Paid in an Asset Account instead of the Liability Account,فعال کردن این گزینه به شما امکان می دهد ثبت کنید -

1. پیش پرداخت های دریافت شده در حساب بدهی به جای حساب دارایی

2. پیش پرداخت های پرداخت شده در حساب دارایی به جای حساب بدهی, +Enabling this will allow creation of multi-currency invoices against single party account in company currency,فعال کردن این امکان ایجاد صورتحساب‌های چند ارزی را در برابر حساب یک طرف به واحد پول شرکت فراهم می‌کند, +End Transit,پایان حمل و نقل, +End of the current subscription period,پایان دوره اشتراک فعلی, +"Enter First and Last name of Employee, based on Which Full Name will be updated. IN transactions, it will be Full Name which will be fetched.",نام و نام خانوادگی کارمند را که بر اساس نام کامل به روز می شود وارد کنید. در معاملات، نام کامل خواهد بود که واکشی می شود., +Enter Manually,ورود دستی, +Enter Serial Nos,شماره های سریال را وارد کنید, +Enter Visit Details,جزئیات بازدید را وارد کنید, +Enter a name for Routing.,یک نام برای مسیریابی وارد کنید., +"Enter a name for the Operation, for example, Cutting.",یک نام برای عملیات وارد کنید، به عنوان مثال، برش., +Enter a name for this Holiday List.,یک نام برای این لیست تعطیلات وارد کنید., +"Enter an Item Code, the name will be auto-filled the same as Item Code on clicking inside the Item Name field.",یک کد مورد را وارد کنید، نام با کلیک کردن در داخل قسمت نام مورد، به طور خودکار مانند کد مورد پر می شود., +Enter each serial no in a new line,هر شماره سریال را در یک خط جدید وارد کنید, +Enter the opening stock units.,واحدهای موجودی افتتاحی را وارد کنید., +Enter the quantity of the Item that will be manufactured from this Bill of Materials.,مقدار آیتمی را که از این صورتحساب مواد تولید می شود وارد کنید., +Enter the quantity to manufacture. Raw material Items will be fetched only when this is set.,مقدار تولید را وارد کنید. آیتم‌های مواد خام فقط زمانی واکشی می شوند که این تنظیم شود., +Error during caller information update,خطا در حین به روز رسانی اطلاعات تماس گیرنده, +Error while posting depreciation entries,خطا هنگام ارسال ورودی های استهلاک, +Error while processing deferred accounting for {0},خطا هنگام پردازش حسابداری معوق برای {0}, +Error while reposting item valuation,خطا هنگام ارسال مجدد ارزیابی مورد, +Errors Notification,اعلان خطاها, +Even invoices with apply tax withholding unchecked will be considered for checking cumulative threshold breach,حتی فاکتورهایی با اعمال کسر مالیات بدون بررسی برای بررسی نقض آستانه تجمعی در نظر گرفته می‌شوند., +Example URL,URL مثال, +Example of a linked document: {0},نمونه ای از یک سند پیوندی: {0}, +Example: Serial No {0} reserved in {1}.,مثال: شماره سریال {0} در {1} رزرو شده است., +Excess Materials Consumed,مواد اضافی مصرف شده, +Excess Transfer,انتقال مازاد, +Exchange Gain Or Loss,سود یا ضرر مبادله, +Exchange Gain/Loss amount has been booked through {0},مبلغ سود/زیان مبادله از طریق {0} رزرو شده است, +Exchange Rate Revaluation Settings,تنظیمات تجدید ارزیابی نرخ ارز, +Excluded DocTypes,DocType های حذف شده, +Exempt Supplies,لوازم معاف, +Expected,انتظار می رود, +Expected Balance Qty,مقدار تراز مورد انتظار, +Expected End Date should be less than or equal to parent task's Expected End Date {0}.,تاریخ پایان مورد انتظار باید کمتر یا مساوی با تاریخ پایان مورد انتظار وظیفه والد {0} باشد., +Expected Stock Value,ارزش موجودی مورد انتظار, +Expected Time Required (In Mins),زمان مورد نیاز مورد انتظار (به دقیقه), +Expiry,انقضا, +Export Data,برون‌بُرد داده ها, +Export Errored Rows,صادر کردن ردیف های خطا, +Export Import Log,لاگ درون‌بُرد برون‌بُرد, +Extra Consumed Qty,مقدار مصرف اضافی, +Extra Job Card Quantity,مقدار کارت کار اضافی, +FIFO Queue vs Qty After Transaction Comparison,صف FIFO در مقابل تعداد پس از مقایسه تراکنش, +"FIFO Stock Queue (qty, rate)",صف موجودی FIFO (تعداد، نرخ), +FIFO/LIFO Queue,صف FIFO/LIFO, +Failed Entries,ورودی های ناموفق, +"Failed to erase demo data, please delete the demo company manually.",داده‌های نمایشی پاک نشد، لطفاً شرکت نمایشی را به صورت دستی حذف کنید., +Failed to setup defaults for country {0}. Please contact support.,تنظیم پیش فرض های کشور {0} انجام نشد. لطفا با پشتیبانی تماس بگیرید., +Failure,شکست, +Failure Description,شرح شکست, +Fetch Based On,واکشی بر اساس, +Fetch Overdue Payments,واکشی پرداخت های معوق, +Fetch Timesheet,واکشی جدول زمانی, +Fetch Value From,واکشی ارزش از, +Fetching exchange rates ...,واکشی نرخ ارز ..., +Filter by Reference Date,فیلتر بر اساس تاریخ مرجع, +Filter on Invoice,فیلتر روی فاکتور, +Filter on Payment,فیلتر در پرداخت, +Filters missing,فیلترها یافت نشدند, +Final Product,محصول نهایی, +Financial Ratios,نسبت های مالی, +Financial Reports,گزارشهای مالی, +Financial Year Begins On,سال مالی شروع می شود, +Financial reports will be generated using GL Entry doctypes (should be enabled if Period Closing Voucher is not posted for all years sequentially or missing) ,گزارش‌های مالی با استفاده از اسناد ثبت دفتر کل ایجاد می‌شوند (اگر کوپن پایان دوره برای همه سال‌ها به‌طور متوالی پست نشده باشد یا مفقود شده باشد، باید فعال شود) , +Finished Good BOM,BOM کالای تمام شده, +Finished Good Item,آیتم کالای تمام شده, +Finished Good Item Qty,تعداد آیتم کالای تمام شده, +Finished Good Item Quantity,تعداد آیتم کالای تمام شده, +Finished Good Item is not specified for service item {0},آیتم کالای تمام شده برای آیتم سرویس مشخص نشده است {0}, +Finished Good Item {0} Qty can not be zero,مقدار آیتم کالای تمام شده {0} تعداد نمی تواند صفر باشد, +Finished Good Item {0} must be a sub-contracted item,آیتم کالای تمام شده {0} باید یک آیتم قرارداد فرعی باشد, +Finished Good Qty,مقدار کالای تمام شده, +Finished Good Quantity ,مقدار کالای تمام شده , +Finished Good UOM,UOM خوب به پایان رسید, +Finished Good {0} does not have a default BOM.,Finished Good {0} BOM پیش فرض ندارد., +Finished Good {0} is disabled.,Finished Good {0} غیرفعال است., +Finished Good {0} must be a stock item.,کالای تمام شده {0} باید یک آیتم موجودی باشد., +Finished Good {0} must be a sub-contracted item.,کالای تمام شده {0} باید یک آیتم قرارداد فرعی باشد., +Finished Goods Based Operating Cost,هزینه عملیاتی بر اساس کالاهای تمام شده, +Finished Goods Item,آیتم کالاهای تمام شده, +Finished Goods Reference,مرجع کالاهای تمام شده, +Finished Goods Value,ارزش کالاهای تمام شده, +Finished Goods based Operating Cost,هزینه عملیاتی بر اساس کالاهای تمام شده, +Finished Item {0} does not match with Work Order {1},مورد تمام شده {0} با دستور کار {1} مطابقت ندارد, +First Response Due,اولین پاسخ به علت, +First Response SLA Failed by {},اولین پاسخ SLA توسط {} انجام نشد, +Fixed Asset Defaults,پیش فرض دارایی های ثابت, +Fixed Time,زمان ثابت, +For Item,برای آیتم, +For Job Card,برای کارت کار, +For Operation,برای عملیات, +For Work Order,برای دستور کار, +For dunning fee and interest,برای هزینه و سود, +"For item {0}, rate must be a positive number. To Allow negative rates, enable {1} in {2}",برای مورد {0}، نرخ باید یک عدد مثبت باشد. برای مجاز کردن نرخ‌های منفی، {1} را در {2} فعال کنید, +For quantity {0} should not be greater than allowed quantity {1},برای مقدار {0} نباید بیشتر از مقدار مجاز {1} باشد, +Forecasting,پیش بینی, +Formula Based Criteria,معیارهای مبتنی بر فرمول, +Free Item Rate,نرخ آیتم رایگان, +From Corrective Job Card,از کارت کار اصلاحی, +From Date and To Date are mandatory,از تاریخ و تا تاریخ اجباری است, +From Delivery Date,از تاریخ تحویل, +From Doctype,از Doctype, +From Due Date,از تاریخ سررسید, +From Opportunity,از فرصت, +From Payment Date,از تاریخ پرداخت, +From Reference Date,از تاریخ مرجع, +From Voucher Detail No,از جزئیات کوپن شماره, +From Voucher No,از کوپن شماره, +From Voucher Type,از نوع کوپن, +From and To dates are required,تاریخ های از و تا تاریخ لازم است, +Full and Final Statement,صورت کامل و نهایی, +GL Balance,تراز دفتر کل, +GL Entry Processing Status,وضعیت پردازش ثبت دفتر کل, +GL reposting index,فهرست ارسال مجدد دفتر کل, +Gain/Loss accumulated in foreign currency account. Accounts with '0' balance in either Base or Account currency,سود/زیان انباشته شده در حساب ارزی. حساب‌هایی با موجودی «0» به ارز پایه یا حساب, +Gain/Loss already booked,سود/باخت قبلا رزرو شده است, +Gain/Loss from Revaluation,سود/زیان ناشی از تجدید ارزیابی, +General Ledger,دفتر کل مرکزی,Warehouse +General and Payment Ledger Comparison,مقایسه دفتر کل مرکزی و پرداخت, +General and Payment Ledger mismatch,عدم تطابق دفتر کل و دفتر پرداخت, +Generate Closing Stock Balance,ایجاد تراز موجودی اختتامیه, +Generate Demo Data for Exploration,داده های نسخه ی نمایشی را برای کاوش ایجاد کنید, +Generate E-Invoice,ایجاد فاکتور الکترونیکی, +Generate Invoice At,ایجاد فاکتور در, +Generating Preview,ایجاد پیش نمایش, +Get Allocations,تخصیص ها را دریافت کنید, +Get Customer Group Details,دریافت جزئیات گروه مشتری, +Get Finished Goods for Manufacture,دریافت کالاهای تمام شده برای تولید, +Get Outstanding Orders,دریافت سفارش‌های معوق, +Get Raw Materials for Purchase,دریافت مواد اولیه برای خرید, +Get Raw Materials for Transfer,دریافت مواد اولیه برای انتقال, +Get Scrap Items,آیتم‌های ضایعات را دریافت کنید, +Get Stock,موجودی دریافت کنید, +Get Sub Assembly Items,دریافت آیتم‌های زیر مونتاژ, +Get Supplier Group Details,جزئیات گروه تامین کننده را دریافت کنید, +Get Timesheets,برگه های زمانی را دریافت کنید, +Get stops from,توقف از, +Getting Scrap Items,دریافت آیتم‌های ضایعات, +Give free item for every N quantity,برای هر N مقدار آیتم رایگان بدهید, +Go back,برگرد, +Go to {0} List,به فهرست {0} بروید, +Goals,اهداف, +Goods,کالاها, +Grant Commission,اعطاء کمیسیون, +Greeting Message,پیام تبریک, +Gross Profit Percent,درصد سود ناخالص, +Gross Purchase Amount should be equal to purchase amount of one single Asset.,مقدار خرید ناخالص باید برابر برای خرید یک دارایی واحد باشد., +Group Same Items,گروه بندی آیتم‌های مشابه, +Growth View,نمای رشد, +Half-yearly,نیم سال, +Has Alternative Item,دارای آیتم جایگزین, +Has Item Scanned,آیتم اسکن شده است, +Have Default Naming Series for Batch ID?,آیا سری نام‌گذاری پیش‌فرض برای Batch ID دارید؟, +Heatmap,نقشه حرارت, +Height (cm),ارتفاع (سانتی متر), +"Hello,",سلام،, +Here are the error logs for the aforementioned failed depreciation entries: {0},در اینجا گزارش های خطا برای ورودی های استهلاک ناموفق فوق الذکر آمده است: {0}, +Here are the options to proceed:,در اینجا گزینه هایی برای ادامه وجود دارد:, +"Here, you can select a senior of this Employee. Based on this, Organization Chart will be populated.",در اینجا، می توانید یک ارشد این کارمند را انتخاب کنید. بر این اساس نمودار سازمانی پر می شود., +"Here, your weekly offs are pre-populated based on the previous selections. You can add more rows to also add public and national holidays individually.",در اینجا، تخفیف‌های هفتگی شما بر اساس انتخاب‌های قبلی از قبل پر شده است. می‌توانید ردیف‌های بیشتری اضافه کنید تا تعطیلات عمومی و ملی را به‌صورت جداگانه اضافه کنید., +"Hi,",سلام،, +Hide Images,مخفی کردن تصاویر, +Holiday Date {0} added multiple times,تاریخ تعطیلات {0} چندین بار اضافه شد, +Hours Spent,ساعت های صرف شده, +How often should Project be updated of Total Purchase Cost ?,هر چند وقت یکبار پروژه باید از هزینه کل خرید به روز شود؟, +I - J,من - جی, +I - K,من - ک, +Idle,بیکار (Idle), +"If an operation is divided into sub operations, they can be added here.",اگر یک عملیات به عملیات فرعی تقسیم شود، می توان آنها را در اینجا اضافه کرد., +"If checked, Rejected Quantity will be included while making Purchase Invoice from Purchase Receipt.",در صورت علامت زدن، مقدار رد شده هنگام تهیه فاکتور خرید از رسید خرید لحاظ می شود., +"If checked, Stock will be reserved on Submit",در صورت علامت زدن، موجودی در ارسال رزرو خواهد شد, +"If checked, picked qty won't automatically be fulfilled on submit of pick list.",اگر علامت زده شود، تعداد انتخاب شده به طور خودکار در ارسال لیست انتخاب انجام نمی شود., +"If checked, the tax amount will be considered as already included in the Paid Amount in Payment Entry",در صورت بررسی، مبلغ مالیات به عنوان مبلغ پرداخت شده در ثبت پرداخت در نظر گرفته می شود, +"If checked, we will create demo data for you to explore the system. This demo data can be erased later.",در صورت علامت زدن، داده‌های نمایشی را برای شما ایجاد می‌کنیم تا سیستم را کاوش کنید. این داده های نمایشی را می توان بعداً پاک کرد., +If enabled then system won't override the picked qty / batches / serial numbers.,اگر فعال باشد، سیستم تعداد / دسته / شماره سریال انتخاب شده را بازنویسی نمی کند., +"If enabled, a print of this document will be attached to each email",در صورت فعال بودن، چاپی از این سند به هر ایمیل پیوست می شود, +"If enabled, additional ledger entries will be made for discounts in a separate Discount Account",در صورت فعال بودن، ورودی های دفتر کل اضافی برای تخفیف در یک حساب تخفیف جداگانه ایجاد می شود, +"If enabled, all files attached to this document will be attached to each email",در صورت فعال بودن، تمام فایل های پیوست شده به این سند به هر ایمیل پیوست می شود, +"If enabled, ledger entries will be posted for change amount in POS transactions",در صورت فعال بودن، ورودی‌های دفتر کل برای مبلغ تغییر در تراکنش‌های POS پست می‌شوند, +"If enabled, the consolidated invoices will have rounded total disabled",در صورت فعال بودن، صورتحساب‌های تلفیقی، کل غیرفعال می‌شوند, +"If enabled, the system will create material requests even if the stock exists in the 'Raw Materials Warehouse'.","اگر فعال باشد، سیستم حتی اگر موجودی در ""انبار مواد خام"" وجود داشته باشد، درخواست های مواد ایجاد می کند.", +"If mentioned, the system will allow only the users with this Role to create or modify any stock transaction earlier than the latest stock transaction for a specific item and warehouse. If set as blank, it allows all users to create/edit back-dated transactions.",در صورت ذکر شده، این سیستم فقط به کاربران دارای این نقش اجازه می‌دهد تا هر تراکنش موجودی را زودتر از آخرین تراکنش موجودی برای یک کالا و انبار خاص ایجاد یا اصلاح کنند. اگر به صورت خالی تنظیم شود، به همه کاربران اجازه می دهد تا تراکنش های قدیمی را ایجاد/ویرایش کنند., +"If not, you can Cancel / Submit this entry",اگر نه، می توانید این ثبت را لغو / ارسال کنید, +"If rate is zero then item will be treated as ""Free Item""","اگر نرخ صفر باشد، آیتم به عنوان ""آیتم رایگان"" تلقی می شود", +"If the BOM results in Scrap material, the Scrap Warehouse needs to be selected.",اگر BOM منجر به مواد قراضه شود، انبار ضایعات باید انتخاب شود., +"If the selected BOM has Operations mentioned in it, the system will fetch all Operations from BOM, these values can be changed.",اگر BOM انتخاب شده دارای عملیات ذکر شده در آن باشد، سیستم تمام عملیات را از BOM واکشی می کند، این مقادیر را می توان تغییر داد., +"If this checkbox is enabled, then the system won’t run the MRP for the available sub-assembly items.",اگر این چک باکس فعال باشد، سیستم MRP را برای آیتم‌های زیر مونتاژ موجود اجرا نمی کند., +If this is undesirable please cancel the corresponding Payment Entry.,اگر این امر نامطلوب است، لطفاً ثبت پرداخت مربوطه را لغو کنید., +"If yes, then this warehouse will be used to store rejected materials",اگر بله، پس از این انبار برای نگهداری مواد رد شده استفاده می شود, +"If you are maintaining stock of this Item in your Inventory, ERPNext will make a stock ledger entry for each transaction of this item.",اگر موجودی این کالا را در موجودی خود نگهداری می کنید، ERPNext برای هر تراکنش این کالا یک ثبت در دفتر کل موجودی ایجاد می کند., +"If you need to reconcile particular transactions against each other, then please select accordingly. If not, all the transactions will be allocated in FIFO order.",اگر نیاز به تطبیق معاملات خاصی با یکدیگر دارید، لطفاً مطابق آن را انتخاب کنید. در غیر این صورت، تمام تراکنش ها به ترتیب FIFO تخصیص می یابد., +"If you still want to proceed, please disable 'Skip Available Sub Assembly Items' checkbox.",اگر همچنان می‌خواهید ادامه دهید، لطفاً کادر انتخاب «صرف نظر از آیتم‌های زیر مونتاژ موجود» را غیرفعال کنید., +"If you still want to proceed, please enable {0}.",اگر همچنان می‌خواهید ادامه دهید، لطفاً {0} را فعال کنید., +"If your CSV uses a different delimiter, add that character here, ensuring no spaces or additional characters are included.",اگر CSV شما از جداکننده دیگری استفاده می‌کند، آن کاراکتر را در اینجا اضافه کنید، مطمئن شوید که هیچ فاصله یا نویسه اضافی در آن وجود ندارد., +Ignore Account Closing Balance,نادیده گرفتن موجودی بسته شدن حساب, +Ignore Available Stock,نادیده گرفتن موجودی در دسترس, +Ignore Closing Balance,نادیده گرفتن تراز اختتامیه, +Ignore Default Payment Terms Template,الگوی شرایط پرداخت پیش‌فرض را نادیده بگیرید, +Ignore Empty Stock,موجودی خالی را نادیده بگیرید, +Ignore Exchange Rate Revaluation Journals,دفتر روزنامه های تجدید ارزیابی نرخ ارز را نادیده بگیرید, +Ignore Pricing Rule is enabled. Cannot apply coupon code.,نادیده گرفتن قانون قیمت گذاری فعال است. نمی توان کد تخفیف را اعمال کرد., +Ignore Voucher Type filter and Select Vouchers Manually,فیلتر نوع کوپن را نادیده بگیرید و کوپن ها را به صورت دستی انتخاب کنید, +Import File,درون‌بُرد فایل, +Import File Errors and Warnings,خطاها و هشدارهای درون‌بُرد فایل, +Import Log Preview,پیش نمایش لاگ درون‌بُرد, +Import Preview,پیش نمایش درون‌بُرد, +Import Progress,پیشرفت درون‌بُرد, +Import Type,نوع درون‌بُرد, +Import Using CSV file,درون‌بُرد با استفاده از فایل CSV, +Import Warnings,هشدارهای درون‌بُرد, +Import from Google Sheets,درون‌بُرد از Google Sheets, +"Importing {0} of {1}, {2}",در حال درون‌بُرد {0} از {1}، {2}, +In House,در خانه, +In Minutes,به دقیقه, +In Party Currency,به ارز طرف, +In Transit Transfer,در انتقال ترانزیت, +In Transit Warehouse,در انبار ترانزیت, +In mins,به دقیقه, +"In row {0} of Appointment Booking Slots: ""To Time"" must be later than ""From Time"".",در ردیف {0} قسمت‌های رزرو قرار ملاقات: «تا زمان» باید دیرتر از «از زمان» باشد., +"In the case of 'Use Multi-Level BOM' in a work order, if the user wishes to add sub-assembly costs to Finished Goods items without using a job card as well the scrap items, then this option needs to be enable.",در مورد «استفاده از BOM چند سطحی» در یک دستور کار، اگر کاربر بخواهد هزینه‌های زیر مونتاژ را بدون استفاده از کارت کار و همچنین آیتم‌های ضایعات به کالاهای نهایی اضافه کند، این گزینه باید فعال شود., +"In this section, you can define Company-wide transaction-related defaults for this Item. Eg. Default Warehouse, Default Price List, Supplier, etc.",در این بخش می‌توانید پیش‌فرض‌های مربوط به تراکنش‌های کل شرکت را برای این آیتم تعریف کنید. به عنوان مثال. انبار پیش فرض، لیست قیمت پیش فرض، تامین کننده و غیره, +Inactive Status,وضعیت غیر فعال, +Include Account Currency,شامل ارز حساب, +Include Default FB Assets,دارایی های پیش فرض FB را شامل شود, +Include Disabled,شامل افراد غیر فعال, +Include Safety Stock in Required Qty Calculation,لحاظ کردن موجودی ایمنی در محاسبه مقدار مورد نیاز, +Include Timesheets in Draft Status,شامل جدول زمانی در وضعیت پیش نویس, +Include Zero Stock Items,شامل آیتم‌های موجودی صفر, +Incoming Call Handling Schedule,برنامه رسیدگی به تماس های ورودی, +Incoming Call Settings,تنظیمات تماس ورودی, +Incoming Rate (Costing),نرخ ورودی (هزینه‌یابی), +Incorrect Balance Qty After Transaction,تعداد موجودی نادرست پس از تراکنش, +Incorrect Batch Consumed,دسته نادرست مصرف شده است, +Incorrect Invoice,فاکتور نادرست, +Incorrect Movement Purpose,هدف جابجایی نادرست, +Incorrect Payment Type,نوع پرداخت نادرست, +Incorrect Serial No Valuation,ارزش گذاری شماره سریال نادرست است, +Incorrect Serial Number Consumed,شماره سریال نادرست مصرف شده است, +Incorrect Stock Value Report,گزارش ارزش موجودی نادرست است, +Incorrect Type of Transaction,نوع تراکنش نادرست, +Incoterm,اینکوترم, +Increase In Asset Life(Months),افزایش عمر دارایی (ماه), +Indent,تورفتگی, +Indirect Expense,هزینه غیر مستقیم, +Individual GL Entry cannot be cancelled.,ثبت انفرادی دفتر کل را نمی توان لغو کرد., +Individual Stock Ledger Entry cannot be cancelled.,ورود فردی به دفتر موجودی را نمی توان لغو کرد., +Insert New Records,درج رکوردهای جدید, +Inspection Rejected,بازرسی رد شد, +Inspection Submission,ارسال بازرسی, +Instruction,دستورالعمل, +Insufficient Capacity,ظرفیت ناکافی, +Insufficient Stock for Batch,موجودی ناکافی برای دسته, +Inter Transfer Reference,مرجع انتقال داخلی, +Interest and/or dunning fee,بهره و/یا هزینه اجناس, +Internal,درونی؛ داخلی, +Internal Customer,مشتری داخلی, +Internal Customer for company {0} already exists,مشتری داخلی برای شرکت {0} از قبل وجود دارد, +Internal Sale or Delivery Reference missing.,مرجع فروش داخلی یا تحویل موجود نیست., +Internal Sales Reference Missing,مرجع فروش داخلی وجود ندارد, +Internal Supplier,تامین کننده داخلی, +Internal Supplier for company {0} already exists,تامین کننده داخلی برای شرکت {0} از قبل وجود دارد, +Internal Transfer Reference Missing,مرجع انتقال داخلی وجود ندارد, +Internal Transfers,نقل و انتقالات داخلی, +Internal transfers can only be done in company's default currency,نقل و انتقالات داخلی فقط با ارز پیش فرض شرکت قابل انجام است, +Invalid,بی اعتبار, +Invalid Auto Repeat Date,تاریخ تکرار خودکار نامعتبر است, +Invalid Cost Center,مرکز هزینه نامعتبر است, +Invalid Delivery Date,تاریخ تحویل نامعتبر است, +Invalid Document,سند نامعتبر, +Invalid Document Type,نوع سند نامعتبر است, +Invalid Formula,فرمول نامعتبر است, +Invalid Group By,گروه نامعتبر توسط, +Invalid Item Defaults,پیش فرض های آیتم نامعتبر, +Invalid Ledger Entries,ورودی های دفتر نامعتبر, +Invalid Primary Role,نقش اصلی نامعتبر است, +Invalid Priority,اولویت نامعتبر است, +Invalid Process Loss Configuration,پیکربندی از دست دادن فرآیند نامعتبر است, +Invalid Purchase Invoice,فاکتور خرید نامعتبر, +Invalid Qty,تعداد نامعتبر است, +Invalid Schedule,زمانبندی نامعتبر است, +Invalid Warehouse,انبار نامعتبر, +Invalid result key. Response:,کلید نتیجه نامعتبر است. واکنش:, +Invalid value {0} for {1} against account {2},مقدار {0} برای {1} در برابر حساب {2} نامعتبر است, +Inventory Dimension,ابعاد موجودی, +Inventory Dimension Negative Stock,ابعاد موجودی منفی, +Inventory Settings,تنظیمات موجودی, +Invoice Cancellation,لغو فاکتور, +Invoice Limit,حد فاکتور, +Invoice Portion (%),سهم فاکتور (%), +Invoice and Billing,فاکتور و صورتحساب, +Invoiced Qty,تعداد فاکتور, +Invoices and Payments have been Fetched and Allocated,فاکتورها و پرداخت ها واکشی و تخصیص داده شده است, +Invoicing Features,ویژگی های صورتحساب, +Is Adjustment Entry,ثبت تعدیل است, +Is Alternative,جایگزین است, +Is Cash or Non Trade Discount,تخفیف نقدی یا غیرتجاری است, +Is Composite Asset,دارایی مرکب است, +Is Corrective Job Card,کارت کار اصلاحی است, +Is Corrective Operation,عملیات اصلاحی است, +Is Expandable,قابل گسترش است, +Is Finished Item,مورد تمام شده است, +Is Fully Depreciated,کاملا مستهلک شده, +Is Group Warehouse,انبار گروه است, +Is Old Subcontracting Flow,جریان پیمانکاری فرعی قدیمی است, +Is Outward,بیرونی است, +Is Period Closing Voucher Entry,ثبت کوپن پایان دوره است, +Is Rate Adjustment Entry (Debit Note),ثبت تعدیل نرخ است (یادداشت بدهی), +Is Recursive,بازگشتی است, +Is Rejected,رد شده است, +Is Rejected Warehouse,انبار مرجوعی است, +Is Scrap Item,آیتم ضایعات است, +Is Short Year,سال کوتاه است, +Is Standard,استاندارد است, +Is Stock Item,آیتم موجودی است, +Is System Generated,سیستم تولید شده است, +Is Template,قالب است, +Issue Analytics,تجزیه و تحلیل مشکل, +Issue Summary,خلاصه مشکل, +Issue a debit note with 0 qty against an existing Sales Invoice,در مقابل فاکتور فروش موجود، یک برگه بدهی با مقدار 0 صادر کنید, +Issuing cannot be done to a location. Please enter employee to issue the Asset {0} to,صدور را نمی توان به یک مکان انجام داد. لطفاً کارمند را وارد کنید تا دارایی {0} را صادر کند, +It can take upto few hours for accurate stock values to be visible after merging items.,ممکن است چند ساعت طول بکشد تا ارزش موجودی دقیق پس از ادغام اقلام قابل مشاهده باشد., +"It's not possible to distribute charges equally when total amount is zero, please set 'Distribute Charges Based On' as 'Quantity'",وقتی مبلغ کل صفر است، نمی توان هزینه ها را به طور مساوی تقسیم کرد، لطفاً «توزیع هزینه ها بر اساس» را به عنوان «مقدار» تنظیم کنید, +Item Code (Final Product),کد آیتم (محصول نهایی), +Item Group wise Discount,تخفیف بر اساس گروه آیتم, +Item Price Settings,تنظیمات قیمت آیتم, +"Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, Batch, UOM, Qty, and Dates.",قیمت آیتم چندین بار بر اساس لیست قیمت، تامین کننده/مشتری، ارز، آیتم، دسته، UOM، مقدار و تاریخ ها ظاهر می شود., +Item Reference,مرجع آیتم, +Item Warehouse based reposting has been enabled.,ارسال مجدد بر اساس انبار مورد فعال شده است., +Item and Warehouse,آیتم و انبار, +Item is removed since no serial / batch no selected.,مورد حذف شده است زیرا هیچ سریال / دسته ای انتخاب نشده است., +Item qty can not be updated as raw materials are already processed.,تعداد مورد را نمی توان به روز کرد زیرا مواد خام قبلاً پردازش شده است., +Item rate has been updated to zero as Allow Zero Valuation Rate is checked for item {0},نرخ مورد به صفر به‌روزرسانی شده است زیرا نرخ ارزش گذاری مجاز صفر برای مورد {0} بررسی می‌شود, +Item valuation reposting in progress. Report might show incorrect item valuation.,ارسال مجدد ارزیابی آیتم در حال انجام است. گزارش ممکن است ارزش گذاری اقلام نادرست را نشان دهد., +Item {0} cannot be added as a sub-assembly of itself,آیتم {0} را نمی توان به عنوان یک زیر مونتاژ از خودش اضافه کرد, +Item {0} cannot be ordered more than {1} against Blanket Order {2}.,مورد {0} را نمی توان بیش از {1} در مقابل سفارش بلانکت {2} سفارش داد., +Item {0} does not exist.,آیتم {0} وجود ندارد., +Item {0} entered multiple times.,آیتم {0} چندین بار وارد شده است., +Item {0} is already reserved/delivered against Sales Order {1}.,مورد {0} قبلاً در برابر سفارش فروش {1} رزرو شده/تحویل شده است., +Item {0} must be a Non-Stock Item,مورد {0} باید یک کالای غیر موجودی باشد, +Item {0} not found in 'Raw Materials Supplied' table in {1} {2},"مورد {0} در جدول ""مواد خام تامین شده"" در {1} {2} یافت نشد", +Item {0} not found.,مورد {0} یافت نشد., +Item {} does not exist.,مورد {} وجود ندارد., +Items & Pricing,آیتم‌ها و قیمت, +Items Catalogue,کاتالوگ آیتم‌ها, +Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}.,موارد را نمی توان به روز کرد زیرا سفارش قرارداد فرعی در برابر سفارش خرید {0} ایجاد شده است., +Items rate has been updated to zero as Allow Zero Valuation Rate is checked for the following items: {0},نرخ اقلام به صفر به‌روزرسانی شده است زیرا نرخ ارزش گذاری مجاز صفر برای موارد زیر بررسی می‌شود: {0}, +Items to Be Repost,مواردی که باید بازنشر شوند, +Items to Order and Receive,آیتم‌های قابل سفارش و دریافت, +Items to Reserve,موارد برای رزرو, +Job Capacity,ظرفیت کاری, +Job Card Operation,عملیات کارت کار, +Job Card Scheduled Time,زمان برنامه ریزی شده کارت کار, +Job Card Scrap Item,آیتم ضایعات کارت کار, +Job Card and Capacity Planning,برنامه ریزی کارت کار و ظرفیت, +Job Paused,کار متوقف شد, +Job: {0} has been triggered for processing failed transactions,شغل: {0} برای پردازش تراکنش های ناموفق فعال شده است, +Joining,پیوستن, +Journal Entries,ورودی های دفتر روزنامه, +Journal Entry for Asset scrapping cannot be cancelled. Please restore the Asset.,ثبت دفتر روزنامه برای حذف دارایی را نمی توان لغو کرد. لطفا دارایی را بازیابی کنید., +Journal Entry type should be set as Depreciation Entry for asset depreciation,نوع ثبت دفتر روزنامه باید به عنوان ورودی استهلاک برای استهلاک دارایی تنظیم شود, +Journals,دفترهای روزنامه, +Key,کلید, +Kindly cancel the Manufacturing Entries first against the work order {0}.,لطفاً ابتدا ورودی‌های تولید را در برابر دستور کار {0} لغو کنید., +"Last Name, Email or Phone/Mobile of the user are mandatory to continue.",نام خانوادگی، ایمیل یا تلفن / تلفن همراه کاربر برای ادامه اجباری است., +Lead -> Prospect,سرنخ -> مشتری بالقوه, +Lead Conversion Time,زمان تبدیل سرنخ, +Lead Owner cannot be same as the Lead Email Address,مالک اصلی نمی تواند با آدرس ایمیل اصلی یکسان باشد, +Lead {0} has been added to prospect {1}.,سرنخ {0} به مشتری بالقوه {1} اضافه شده است., +Leaderboard,تابلوی امتیازات, +Leads,سرنخ ها, +Learn Accounting,حسابداری یاد بگیرید, +Learn Inventory Management,مدیریت لیست موجودی را یاد بگیرید, +Learn Manufacturing,یادگیری تولید, +Learn Procurement,تدارکات را بیاموزید, +Learn Project Management,مدیریت پروژه را یاد بگیرید, +Learn Sales Management,مدیریت فروش را یاد بگیرید, +Ledger Merge,ادغام دفتر کل, +Ledger Merge Accounts,حساب های ادغام دفتر کل, +Ledgers,دفتر کل, +Legend,افسانه, +Length,طول, +Length (cm),طول (سانتی متر), +Less than 12 months.,کمتر از 12 ماه., +Level (BOM),سطح (BOM), +Limit timeslot for Stock Reposting,محدودیت زمانی برای ارسال مجدد موجودی, +Limits don't apply on,محدودیت ها اعمال نمی شود, +Link a new bank account,پیوند یک حساب بانکی جدید, +Link with Customer,پیوند با مشتری, +Link with Supplier,پیوند با تامین کننده, +Linked with submitted documents,مرتبط با اسناد ارسالی, +Linking Failed,پیوند ناموفق بود, +Linking to Customer Failed. Please try again.,پیوند به مشتری انجام نشد. لطفا دوباره تلاش کنید., +Linking to Supplier Failed. Please try again.,پیوند به تامین کننده انجام نشد. لطفا دوباره تلاش کنید., +Links,پیوندها, +Loading import file...,در حال بارگیری فایل درون‌بُرد..., +Locked,قفل شده است, +Log Entries,ورودی های لاگ, +Lost Quotations,پیش فاکتور های از دست رفته, +Lost Quotations %,% پیش فاکتور های از دست رفته, +Lost Reasons are required in case opportunity is Lost.,در صورت از دست رفتن فرصت، دلایل از دست رفتن مورد نیاز است., +Lost Value,ارزش از دست رفته, +Lost Value %,مقدار از دست رفته %, +Machine Type,نوع ماشین, +Main Cost Center,مرکز هزینه اصلی, +Main Cost Center {0} cannot be entered in the child table,مرکز هزینه اصلی {0} را نمی توان در جدول فرزند وارد کرد, +Maintain Asset,حفظ دارایی, +Maintenance Details,جزئیات تعمیر و نگهداری, +Make , بسازید, +Make Asset Movement,جابجایی دارایی را ایجاد کنید, +Make Quotation,پیش فاکتور ایجاد کنید, +Make Return Entry,ایجاد ثبت بازگشت, +Make Serial No / Batch from Work Order,شماره سریال / دسته از دستور کار را بسازید, +Make {0} Variant,ایجاد {0} گونه, +Make {0} Variants,ایجاد {0} گونه, +Manage,مدیریت, +Mandatory Accounting Dimension,بعد حسابداری اجباری, +Mandatory Depends On,اجباری بستگی دارد, +Mandatory Field,فیلد اجباری, +Mandatory Section,بخش اجباری, +Manual Inspection,بازرسی دستی, +Manufacture Sub-assembly in Operation,تولید زیر مونتاژ در عملیات, +Manufacturing Type,نوع تولید, +Mapping Purchase Receipt ...,نگاشت رسید خرید ..., +Mapping Subcontracting Order ...,نگاشت سفارش پیمانکاری فرعی ..., +Mapping {0} ...,نگاشت {0}..., +Mark As Closed,علامت گذاری به عنوان بسته شده, +Material Returned from WIP,مواد برگردانده شده از «کار در حال انجام», +Material Transfer (In Transit),انتقال مواد (در حال حمل و نقل), +Materials are already received against the {0} {1},مواد قبلاً در مقابل {0} {1} دریافت شده است, +Materials needs to be transferred to the work in progress warehouse for the job card {0},برای کارت کار باید مواد به انبار کار در حال انجام انتقال داده شود {0}, +Max Qty (As Per Stock UOM),حداکثر تعداد (بر اساس موجودی UOM), +Maximum Net Rate,حداکثر نرخ خالص, +Maximum Payment Amount,حداکثر مبلغ پرداختی, +Maximum Value,حداکثر ارزش, +Maximum quantity scanned for item {0}.,حداکثر مقدار اسکن شده برای مورد {0}., +Meeting,ملاقات, +Mention if non-standard Receivable account,اگر حساب دریافتنی غیر استاندارد است را ذکر کنید, +Merge Invoices Based On,ادغام فاکتورها بر اساس, +Merge Progress,ادغام پیشرفت, +Merge Similar Account Heads,ادغام سران حساب های مشابه, +Merge taxes from multiple documents,ادغام مالیات از اسناد متعدد, +Merged,ادغام شد, +"Merging is only possible if following properties are same in both records. Is Group, Root Type, Company and Account Currency",ادغام تنها در صورتی امکان پذیر است که ویژگی های زیر در هر دو رکورد یکسان باشند. گروه، نوع ریشه، شرکت و ارز حساب است, +Merging {0} of {1},ادغام {0} از {1}, +Min Qty (As Per Stock UOM),حداقل تعداد (بر اساس موجودی UOM), +Min Qty should be greater than Recurse Over Qty,Min Qty باید بیشتر از Recurse Over Qty باشد, +Minimum Net Rate,حداقل نرخ خالص, +Minimum Payment Amount,حداقل مبلغ پرداختی, +Minimum Value,حداقل ارزش, +Mismatch,عدم تطابق, +Missing,جا افتاده, +Missing Asset,دارایی گمشده, +Missing Cost Center,مرکز هزینه جا افتاده, +Missing Finance Book,کتاب مالی جا افتاده, +Missing Finished Good,از دست رفته به پایان رسید, +Missing Formula,فرمول جا افتاده, +Missing Items,آیتم‌های جا افتاده, +Missing Payments App,برنامه پرداخت وجود ندارد, +Missing Serial No Bundle,باندل شماره سریال جا افتاده, +Missing value,مقدار از دست رفته, +Modified By,تغییر داده شده توسط, +Modified On,اصلاح شده در, +Module Settings,تنظیمات ماژول, +Move Stock,انتقال موجودی, +Move to Cart,جابجایی به سبد خرید, +Movement,جابجایی, +Moving up in tree ...,بالا رفتن در درخت..., +Multi-level BOM Creator,ایجاد کننده BOM چند سطحی, +Multiple Loyalty Programs found for Customer {}. Please select manually.,چندین برنامه وفاداری برای مشتری {} پیدا شد. لطفا به صورت دستی انتخاب کنید, +Multiple Warehouse Accounts,چندین حساب انبار, +Multiple items cannot be marked as finished item,چند مورد را نمی توان به عنوان مورد تمام شده علامت گذاری کرد, +Must be a publicly accessible Google Sheets URL and adding Bank Account column is necessary for importing via Google Sheets,باید یک URL کاربرگ‌نگار Google برای عموم قابل دسترسی باشد و افزودن ستون حساب بانکی برای درون‌بُرد از طریق Google Sheets ضروری است., +Named Place,مکان نامگذاری شده, +Naming Series and Price Defaults,نام گذاری سری ها و پیش فرض های قیمت, +Net total calculation precision loss,خالص از دست دادن دقت محاسبه کل, +New Balance In Account Currency,تراز جدید به ارز حساب, +New Event,رویداد جدید, +New Note,یادداشت جدید, +New Task,وظیفه جدید, +New Version,نسخه جدید, +Newsletter,خبرنامه, +No Answer,بدون پاسخ, +No Customers found with selected options.,هیچ مشتری با گزینه های انتخاب شده یافت نشد., +No Items selected for transfer.,هیچ موردی برای انتقال انتخاب نشده است., +No Matching Bank Transactions Found,هیچ تراکنش بانکی منطبقی یافت نشد, +No Notes,بدون یادداشت, +No Outstanding Invoices found for this party,هیچ صورتحساب معوقی برای این طرف یافت نشد, +No POS Profile found. Please create a New POS Profile first,هیچ نمایه POS یافت نشد. لطفا ابتدا یک نمایه POS جدید ایجاد کنید, +No Records for these settings.,هیچ رکوردی برای این تنظیمات وجود ندارد., +No Stock Available Currently,موجودی در حال حاضر موجود نیست, +No Summary,بدون خلاصه, +No Tax Withholding data found for the current posting date.,هیچ داده کسر مالیات برای تاریخ ارسال فعلی یافت نشد., +No Terms,بدون شرایط, +No Unreconciled Invoices and Payments found for this party and account,هیچ فاکتور و پرداخت ناسازگاری برای این طرف و حساب پیدا نشد, +No Unreconciled Payments found for this party,هیچ پرداخت ناسازگاری برای این طرف یافت نشد, +No Work Orders were created,هیچ دستور کار ایجاد نشد, +No additional fields available,هیچ فیلد اضافی در دسترس نیست, +No billing email found for customer: {0},هیچ ایمیل صورتحساب برای مشتری پیدا نشد: {0}, +No data found. Seems like you uploaded a blank file,داده ای یافت نشد. به نظر می رسد شما یک فایل خالی آپلود کرده اید, +No employee was scheduled for call popup,هیچ کارمندی برای فراخوانی برنامه ریزی نشده بود, +No failed logs,هیچ لاگ ناموفقی نیست, +No item available for transfer.,هیچ آیتمی برای انتقال موجود نیست., +No items are available in sales orders {0} for production,هیچ موردی در سفار‌ش‌های فروش {0} برای تولید موجود نیست, +No items are available in the sales order {0} for production,هیچ موردی در سفارش فروش {0} برای تولید موجود نیست, +No matches occurred via auto reconciliation,هیچ همخوانی ای از طریق تطبیق خودکار رخ نداد, +No more children on Left,دیگر هیچ فرزندی در سمت چپ وجود ندارد, +No more children on Right,دیگر هیچ فرزندی در سمت راست وجود ندارد, +No of Docs,شماره اسناد, +No of Employees,تعداد کارمندان, +No of Months (Expense),تعداد ماه ها (هزینه), +No of Months (Revenue),تعداد ماه ها (درآمد), +No open event,رویداد باز وجود ندارد, +No open task,هیچ وظیفه بازی نیست, +No outstanding {0} found for the {1} {2} which qualify the filters you have specified.,هیچ {0} معوقاتی برای {1} {2} که واجد شرایط فیلترهایی است که شما مشخص کرده اید، یافت نشد., +No primary email found for customer: {0},ایمیل اصلی برای مشتری پیدا نشد: {0}, +No records found in Allocation table,هیچ رکوردی در جدول تخصیص یافت نشد, +No records found in the Invoices table,هیچ رکوردی در جدول فاکتورها یافت نشد, +No records found in the Payments table,هیچ رکوردی در جدول پرداخت ها یافت نشد, +No stock transactions can be created or modified before this date.,هیچ تراکنش موجودیی را نمی توان قبل از این تاریخ ایجاد یا تغییر داد., +No {0} Accounts found for this company.,هیچ حساب {0} برای این شرکت یافت نشد., +No.,شماره, +No. of Employees,تعداد کارمندان, +No. of parallel job cards which can be allowed on this workstation. Example: 2 would mean this workstation can process production for two Work Orders at a time.,تعداد کارت کارهای موازی که می توانند در این ایستگاه کاری مجاز باشند. مثال: 2 به این معنی است که این ایستگاه کاری می تواند تولید را برای دو دستور کار در یک زمان پردازش کند., +Note: Automatic log deletion only applies to logs of type Update Cost,توجه: حذف خودکار لاگ فقط برای گزارش‌هایی از نوع به‌روزرسانی هزینه اعمال می‌شود, +"Note: To merge the items, create a separate Stock Reconciliation for the old item {0}",توجه: برای ادغام آیتم‌ها، یک تطبیق موجودی جداگانه برای آیتم قدیمی {0} ایجاد کنید, +Notes HTML,یادداشت های HTML, +Notification,اعلان, +Notification Settings,تنظیمات اعلان, +Notify Reposting Error to Role,خطای ارسال مجدد به نقش را اطلاع دهید, +Number of Days,تعداد روزها, +Numeric,عددی, +Numeric Inspection,بازرسی عددی, +Off,خاموش, +Offsetting Account,تسویه حساب, +Offsetting for Accounting Dimension,جبران برای بعد حسابداری, +On Paid Amount,با مبلغ پرداخت شده, +On This Date,در این تاریخ, +On Track,در مسیر, +"On expanding a row in the Items to Manufacture table, you'll see an option to 'Include Exploded Items'. Ticking this includes raw materials of the sub-assembly items in the production process.","با گسترش یک ردیف در جدول Items to Manufacture، گزینه ای برای ""شامل آیتم‌های گسترده شده"" را مشاهده خواهید کرد. تیک زدن این شامل مواد اولیه آیتم‌های زیر مونتاژ در فرآیند تولید می شود.", +Once the Work Order is Closed. It can't be resumed.,هنگامی که دستور کار بسته شد. نمی توان آن را از سر گرفت., +Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload,برای درون‌بُرد داده ها فقط می توان از فایل های CSV و Excel استفاده کرد. لطفاً فرمت فایلی را که می‌خواهید آپلود کنید بررسی کنید, +Only Deduct Tax On Excess Amount , فقط از مقدار مازاد مالیات کسر کنید, +Only Include Allocated Payments,فقط شامل پرداخت های اختصاص داده شده است, +Only Parent can be of type {0},فقط والد می توانند از نوع {0} باشند, +Only existing assets,فقط دارایی های موجود, +"Only one Subcontracting Order can be created against a Purchase Order, cancel the existing Subcontracting Order to create a new one.",فقط یک سفارش پیمانکاری فرعی را می توان در مقابل یک سفارش خرید ایجاد کرد، برای ایجاد یک سفارش جدید، سفارش پیمانکاری فرعی موجود را لغو کنید., +Only {0} are supported,فقط {0} پشتیبانی می شود, +Open Activities HTML,Activity HTML را باز کنید, +Open Call Log,لاگ تماس را باز کنید, +Open Events,رویدادهای باز, +Open Sales Orders,باز کردن سفارش‌های فروش, +Open Task,وظیفه باز, +Open Tasks,وظایف باز, +Opening & Closing,افتتاحیه و اختتامیه, +Opening Accumulated Depreciation must be less than or equal to {0},استهلاک انباشته افتتاحیه باید کمتر یا مساوی با {0} باشد., +Opening Entry can not be created after Period Closing Voucher is created.,پس از ایجاد کوپن بسته شدن دوره، ورودی باز نمی تواند ایجاد شود., +Opening Purchase Invoices have been created.,فاکتورهای خرید افتتاحیه ایجاد شده است., +Opening Sales Invoices have been created.,فاکتورهای فروش افتتاحیه ایجاد شده است., +Operating Cost Per BOM Quantity,هزینه عملیاتی به ازای هر مقدار BOM, +Operation time does not depend on quantity to produce,زمان عملیات به مقدار تولید بستگی ندارد, +Opportunity Amount (Company Currency),مقدار فرصت (ارز شرکت), +Opportunity Owner,صاحب فرصت, +Opportunity Source,منبع فرصت, +Opportunity Summary by Sales Stage,خلاصه فرصت بر اساس مرحله فروش, +Opportunity Summary by Sales Stage ,خلاصه فرصت بر اساس مرحله فروش , +Opportunity Value,ارزش فرصت, +Order Status,وضعیت سفارش, +Other Info,سایر اطلاعات, +Out of stock,تمام شده, +Over Receipt,بیش از رسید, +Over Receipt/Delivery of {0} {1} ignored for item {2} because you have {3} role.,بیش از رسید/تحویل {0} {1} برای مورد {2} نادیده گرفته شد زیرا شما نقش {3} را دارید., +Over Transfer Allowance,بیش از کمک هزینه انتقال, +Overbilling of {0} {1} ignored for item {2} because you have {3} role.,اضافه صورتحساب {0} {1} برای مورد {2} نادیده گرفته شد زیرا شما نقش {3} را دارید., +Overbilling of {} ignored because you have {} role.,پرداخت بیش از حد {} نادیده گرفته شد زیرا شما نقش {} را دارید., +Overdue Payment,پرداخت معوقه, +Overdue Payments,پرداخت های معوق, +Overdue Tasks,وظایف عقب افتاده, +Overview,بررسی اجمالی, +PDF Name,نام PDF, +POS Closing Failed,بسته شدن POS ناموفق بود, +POS Closing failed while running in a background process. You can resolve the {0} and retry the process again.,بسته شدن POS هنگام اجرا در یک فرآیند پس‌زمینه انجام نشد. می توانید {0} را حل کنید و دوباره این فرآیند را امتحان کنید., +POS Invoices will be consolidated in a background process,فاکتورهای POS در یک فرآیند پس زمینه تلفیق می شوند, +POS Invoices will be unconsolidated in a background process,فاکتورهای POS در یک فرآیند پس زمینه تجمیع نمی شوند, +POS Profile {} contains Mode of Payment {}. Please remove them to disable this mode.,نمایه POS {} شامل حالت پرداخت {} است. لطفاً آنها را حذف کنید تا این حالت غیرفعال شود., +POS Search Fields,فیلدهای جستجوی POS, +POS Setting,تنظیمات POS, +Package No(s) already in use. Try from Package No {0},شماره (های) بسته در حال حاضر در حال استفاده است. از بسته شماره {0} امتحان کنید, +Packaging Slip From Delivery Note,برگه بسته بندی از یادداشت تحویل, +Packed Items cannot be transferred internally,اقلام بسته بندی شده را نمی توان به صورت داخلی منتقل کرد, +Packed Qty,تعداد بسته بندی شده, +Page Break After Each SoA,شکست صفحه پس از هر SoA, +Paid Amount After Tax,مبلغ پرداختی پس از کسر مالیات, +Paid Amount After Tax (Company Currency),مبلغ پرداختی پس از مالیات (ارز شرکت), +Paid From Account Type,پرداخت از نوع حساب, +Paid To Account Type,پرداخت به نوع حساب, +Pallets,پالت, +Parameter Group,گروه پارامتر, +Parameter Group Name,نام گروه پارامتر, +Parcel Template,قالب بسته, +Parcel Template Name,نام قالب بسته, +Parcel weight cannot be 0,وزن بسته نمی تواند 0 باشد, +Parcels,بسته ها, +Parent Account Missing,حساب والدین جا افتاده است, +Parent Document,سند والد, +Parent Item {0} must not be a Fixed Asset,مورد اصلی {0} نباید دارایی ثابت باشد, +Parent Row No,شماره ردیف والد, +Parent Task {0} is not a Template Task,وظیفه والد {0} یک وظیفه الگو نیست, +Partial Material Transferred,مواد جزئی منتقل شد, +Partial Stock Reservation,رزرو جزئی موجودی, +Partial Success,موفقیت جزئی, +"Partial stock can be reserved. For example, If you have a Sales Order of 100 units and the Available Stock is 90 units then a Stock Reservation Entry will be created for 90 units. ",موجودی جزئی قابل رزرو است. به عنوان مثال، اگر شما یک سفارش فروش 100 واحدی دارید و موجودی در دسترس 90 واحد است، یک ورودی رزرو موجودی برای 90 واحد ایجاد می شود. , +Partially Delivered,تا حدی تحویل شد, +Partially Reconciled,تا حدی تطبیق کرد, +Partially Reserved,تا حدی رزرو شده است, +Partly Paid,تا حدی پرداخت شده, +Partly Paid and Discounted,تا حدودی پرداخت شده و با تخفیف, +Partnership,شراکت, +Party Account No. (Bank Statement),شماره حساب طرف (صورتحساب بانکی), +Party Account {0} currency ({1}) and document currency ({2}) should be same,واحد پول حساب طرف {0} ({1}) و واحد پول سند ({2}) باید یکسان باشند, +Party IBAN (Bank Statement),طرف IBAN (صورتحساب بانکی), +Party Item Code,کد آیتم طرف, +Party Link,لینک طرف, +Party Name/Account Holder (Bank Statement),نام طرف / دارنده حساب (صورتحساب بانکی), +Party Specific Item,مورد خاص طرف, +Party Type and Party is required for Receivable / Payable account {0},نوع طرف و طرف برای حساب دریافتنی / پرداختنی {0} لازم است, +Party can only be one of {0},طرف فقط می تواند یکی از {0} باشد, +Passport Details,مشخصات پاسپورت, +Pause Job,مکث کار, +Paused,مکث کرد, +Payment Amount (Company Currency),مبلغ پرداختی (ارز شرکت), +"Payment Entry {0} is linked against Order {1}, check if it should be pulled as advance in this invoice.",ثبت پرداخت {0} با سفارش {1} مرتبط است، بررسی کنید که آیا باید به عنوان پیش پرداخت در این فاکتور آورده شود., +Payment Ledger,دفتر کل پرداخت, +Payment Ledger Balance,تراز دفتر کل پرداخت, +Payment Ledger Entry,ثبت دفتر پرداخت, +Payment Limit,محدودیت پرداخت, +Payment Reconciliation Allocation,تخصیص تطبیق پرداخت, +Payment Reconciliation Job: {0} is running for this party. Can't reconcile now.,کار تطبیق پرداخت: {0} برای این طرف اجرا می شود. الان نمیشه تطبیق کرد, +Payment Reconciliations,تطبیق های پرداخت, +Payment Request took too long to respond. Please try requesting for payment again.,پاسخ درخواست پرداخت خیلی طول کشید. لطفاً دوباره درخواست پرداخت کنید., +Payment Requests cannot be created against: {0},درخواست های پرداخت را نمی توان در مقابل: {0} ایجاد کرد, +Payment Terms Status for Sales Order,وضعیت شرایط پرداخت برای سفارش فروش, +Payment Terms from orders will be fetched into the invoices as is,شرایط پرداخت از سفارش‌ها همانطور که هست در فاکتورها آورده می شود, +Payment Unlink Error,خطای لغو پیوند پرداخت, +Payment of {0} received successfully.,پرداخت {0} با موفقیت دریافت شد., +Payment of {0} received successfully. Waiting for other requests to complete...,پرداخت {0} با موفقیت دریافت شد. در انتظار تکمیل درخواست های دیگر..., +Payment request failed,درخواست پرداخت انجام نشد, +Payment term {0} not used in {1},مدت پرداخت {0} در {1} استفاده نشده است, +Pending processing,در انتظار پردازش, +Per Received,هر دریافت شده, +Percentage (%),درصد (%), +Percentage you are allowed to order beyond the Blanket Order quantity.,درصدی که مجاز به سفارش آن هستید فراتر از مقدار سفارش کلی., +Percentage you are allowed to sell beyond the Blanket Order quantity.,درصدی که مجاز به فروش آن هستید فراتر از مقدار سفارش کلی., +Period Closed,دوره بسته است, +Period Closing Settings,تنظیمات اختتامیه دوره, +Period Details,جزئیات دوره, +Phone Ext.,تلفن خارجی, +Pick List Incomplete,فهرست انتخابی ناقص است, +Pick Manually,انتخاب دستی, +Pick Serial / Batch Based On,انتخاب سریال / دسته بر اساس, +Pick Serial / Batch No,انتخاب سریال / شماره دسته, +Picked Qty (in Stock UOM),تعداد انتخاب شده (در انبار UOM), +Pickup,سوار کردن, +Pickup Contact Person,شخص تماس وانت, +Pickup Date,تاریخ تحویل, +Pickup Date cannot be before this day,تاریخ تحویل نمی‌تواند قبل از این روز باشد, +Pickup From,وانت از, +Pickup To time should be greater than Pickup From time,Pickup To time باید بیشتر از Pickup From Time باشد, +Pickup Type,نوع پیکاپ, +Pickup from,وانت از, +Pickup to,وانت به, +Pipeline By,خط لوله توسط, +Plaid Link Failed,پیوند Plaid ناموفق بود, +Plaid Link Refresh Required,بازخوانی پیوند شطرنجی مورد نیاز است, +Plaid Link Updated,پیوند شطرنجی به روز شد, +Plan to Request Qty,برنامه ریزی برای مقدار درخواست, +Please Set Priority,لطفا اولویت را تعیین کنید, +Please Specify Account,لطفا حساب را مشخص کنید, +Please add 'Supplier' role to user {0}.,"لطفا نقش ""تامین کننده"" را به کاربر {0} اضافه کنید.", +Please add Request for Quotation to the sidebar in Portal Settings.,لطفاً درخواست برای پیش فاکتور را به نوار کناری در تنظیمات پورتال اضافه کنید., +Please add Root Account for - {0},لطفاً حساب ریشه برای - {0} اضافه کنید, +Please add atleast one Serial No / Batch No,لطفاً حداقل یک شماره سریال / شماره دسته اضافه کنید, +Please add the Bank Account column,لطفا ستون حساب بانکی را اضافه کنید, +Please add the account to root level Company - {0},لطفاً حساب را به شرکت سطح ریشه اضافه کنید - {0}, +Please add {1} role to user {0}.,لطفاً نقش {1} را به کاربر {0} اضافه کنید., +Please adjust the qty or edit {0} to proceed.,لطفاً تعداد را تنظیم کنید یا برای ادامه {0} را ویرایش کنید., +Please attach CSV file,لطفا فایل CSV را پیوست کنید, +Please cancel and amend the Payment Entry,لطفاً ثبت پرداخت را لغو و اصلاح کنید, +Please cancel payment entry manually first,لطفاً ابتدا ثبت پرداخت را به صورت دستی لغو کنید, +Please cancel related transaction.,لطفا تراکنش مربوطه را لغو کنید., +Please check Process Deferred Accounting {0} and submit manually after resolving errors.,لطفاً Process Deferred Accounting {0} را بررسی کنید و پس از رفع خطاها را به صورت دستی ارسال کنید., +Please check either with operations or FG Based Operating Cost.,لطفاً با عملیات یا هزینه عملیاتی مبتنی بر FG بررسی کنید., +Please check the error message and take necessary actions to fix the error and then restart the reposting again.,لطفاً پیام خطا را بررسی کنید و اقدامات لازم را برای رفع خطا انجام دهید و سپس ارسال مجدد را مجدداً راه‌اندازی کنید., +Please check your email to confirm the appointment,لطفا ایمیل خود را برای تایید قرار ملاقات بررسی کنید, +Please contact any of the following users to extend the credit limits for {0}: {1},لطفاً برای تمدید محدودیت اعتبار برای {0} با هر یک از کاربران زیر تماس بگیرید: {1}, +Please contact any of the following users to {} this transaction.,لطفاً با هر یک از کاربران زیر برای {} این تراکنش تماس بگیرید., +Please contact your administrator to extend the credit limits for {0}.,لطفاً برای تمدید محدودیت اعتبار برای {0} با سرپرست خود تماس بگیرید., +Please create Landed Cost Vouchers against Invoices that have 'Update Stock' enabled.,لطفاً در برابر فاکتورهایی که «به‌روزرسانی موجودی» را فعال کرده‌اند، کوپن‌های هزینه تمام شده تا درب انبار ایجاد کنید., +Please create a new Accounting Dimension if required.,لطفاً در صورت نیاز یک بعد حسابداری جدید ایجاد کنید., +Please create purchase from internal sale or delivery document itself,لطفا خرید را از فروش داخلی یا سند تحویل خود ایجاد کنید, +"Please delete Product Bundle {0}, before merging {1} into {2}",لطفاً قبل از ادغام {1} در {2}، باندل محصول {0} را حذف کنید, +Please do not book expense of multiple assets against one single Asset.,لطفا هزینه چند دارایی را در مقابل یک دارایی ثبت نکنید., +Please enable only if the understand the effects of enabling this.,لطفاً فقط در صورتی فعال کنید که تأثیرات فعال کردن آن را درک کنید., +Please enable {0} in the {1}.,لطفاً {0} را در {1} فعال کنید., +Please enable {} in {} to allow same item in multiple rows,لطفاً {} را در {} فعال کنید تا یک مورد در چندین ردیف مجاز باشد, +Please ensure {} account is a Balance Sheet account.,لطفاً مطمئن شوید که حساب {} یک حساب ترازنامه است., +Please ensure {} account {} is a Receivable account.,لطفاً مطمئن شوید که {} حساب {} یک حساب دریافتنی است., +Please enter Root Type for account- {0},لطفاً نوع ریشه را برای حساب وارد کنید- {0}, +Please enter Serial Nos,لطفا شماره های سریال را وارد کنید, +Please enter Shipment Parcel information,لطفا اطلاعات بسته حمل و نقل را وارد کنید, +Please enter Stock Items consumed during the Repair.,لطفاً آیتم‌های موجودی مصرف شده در طول تعمیر را وارد کنید., +Please enter mobile number first.,لطفا ابتدا شماره موبایل را وارد کنید, +Please enter quantity for item {0},لطفاً مقدار مورد {0} را وارد کنید, +Please enter serial nos,لطفا شماره سریال را وارد کنید, +"Please first set Last Name, Email and Phone for the user",لطفا ابتدا نام خانوادگی، ایمیل و تلفن را برای کاربر تنظیم کنید, +Please fix overlapping time slots for {0},لطفاً بازه های زمانی همپوشانی را برای {0} اصلاح کنید, +Please fix overlapping time slots for {0}.,لطفاً بازه های زمانی همپوشانی را برای {0} برطرف کنید., +Please import accounts against parent company or enable {} in company master.,لطفاً حساب‌ها را در مقابل شرکت مادر وارد کنید یا {} را در شرکت اصلی فعال کنید., +Please make sure the file you are using has 'Parent Account' column present in the header.,لطفاً مطمئن شوید که فایلی که استفاده می‌کنید دارای ستون «حساب والد» در سرصفحه باشد., +Please mention 'Weight UOM' along with Weight.,"لطفا ""وزن UOM"" را همراه با وزن ذکر کنید.", +Please mention the Current and New BOM for replacement.,لطفاً BOM فعلی و جدید را برای جایگزینی ذکر کنید., +Please rectify and try again.,لطفاً اصلاح کنید و دوباره امتحان کنید., +Please refresh or reset the Plaid linking of the Bank {}.,لطفاً پیوند Plaid بانک {} را بازخوانی یا بازنشانی کنید., +Please save before proceeding.,لطفا قبل از ادامه ذخیره کنید., +Please select Bank Account,لطفا حساب بانکی را انتخاب کنید, +Please select Finished Good Item for Service Item {0},لطفاً آیتم کالای تمام شده را برای آیتم سرویس {0} انتخاب کنید, +Please select Serial/Batch Nos to reserve or change Reservation Based On to Qty.,لطفاً شماره‌های سریال/دسته را برای رزرو انتخاب کنید یا رزرو براساس تعداد را تغییر دهید., +Please select Subcontracting Order instead of Purchase Order {0},لطفاً به جای سفارش خرید، سفارش قرارداد فرعی را انتخاب کنید {0}, +Please select Unrealized Profit / Loss account or add default Unrealized Profit / Loss account account for company {0},لطفاً حساب سود / زیان تحقق نیافته را انتخاب کنید یا حساب سود / زیان پیش فرض را برای شرکت اضافه کنید {0}, +Please select a Subcontracting Purchase Order.,لطفاً سفارش خرید پیمانکاری فرعی را انتخاب کنید., +Please select a Warehouse,لطفاً یک انبار انتخاب کنید, +Please select a Work Order first.,لطفاً ابتدا یک دستور کار را انتخاب کنید., +Please select a country,لطفا یک کشور را انتخاب کنید, +Please select a customer for fetching payments.,لطفاً یک مشتری را برای واکشی پرداخت ها انتخاب کنید., +Please select a date,لطفا تاریخ را انتخاب کنید, +Please select a date and time,لطفا تاریخ و زمان را انتخاب کنید, +Please select a row to create a Reposting Entry,لطفاً یک ردیف برای ایجاد یک ورودی ارسال مجدد انتخاب کنید, +Please select a supplier for fetching payments.,لطفاً یک تامین کننده برای واکشی پرداخت ها انتخاب کنید., +Please select a valid Purchase Order that has Service Items.,لطفاً یک سفارش خرید معتبر که دارای آیتم‌های خدماتی است انتخاب کنید., +Please select a valid Purchase Order that is configured for Subcontracting.,لطفاً یک سفارش خرید معتبر که برای قرارداد فرعی پیکربندی شده است، انتخاب کنید., +Please select an item code before setting the warehouse.,لطفاً قبل از تنظیم انبار یک کد آیتم را انتخاب کنید., +Please select items,لطفا آیتم‌ها را انتخاب کنید, +Please select items to unreserve.,لطفا آیتم‌ها را برای لغو رزرو انتخاب کنید., +Please select only one row to create a Reposting Entry,لطفاً فقط یک ردیف را برای ایجاد یک ورودی ارسال مجدد انتخاب کنید, +Please select rows to create Reposting Entries,لطفاً ردیف‌هایی را برای ایجاد ورودی‌های ارسال مجدد انتخاب کنید, +Please select the required filters,لطفا فیلترهای مورد نیاز را انتخاب کنید, +Please select valid document type.,لطفا نوع سند معتبر را انتخاب کنید., +Please set Account,لطفا حساب را تنظیم کنید, +Please set Accounting Dimension {} in {},لطفاً بعد حسابداری {} را در {} تنظیم کنید, +Please set Email/Phone for the contact,لطفا ایمیل/تلفن را برای مخاطب تنظیم کنید, +Please set Fiscal Code for the customer '%s',"لطفاً کد مالی را برای مشتری ""%s"" تنظیم کنید", +Please set Fixed Asset Account in {} against {}.,لطفاً حساب دارایی ثابت را در {} در مقابل {} تنظیم کنید., +Please set Parent Row No for item {0},لطفاً شماره ردیف والد را برای آیتم {0} تنظیم کنید, +Please set Root Type,لطفا Root Type را تنظیم کنید, +Please set Tax ID for the customer '%s',"لطفاً شناسه مالیاتی را برای مشتری ""%s"" تنظیم کنید", +Please set VAT Accounts in {0},لطفاً حساب‌های VAT را در {0} تنظیم کنید, +"Please set Vat Accounts for Company: ""{0}"" in UAE VAT Settings","لطفاً حساب‌های مالیات بر ارزش افزوده را برای شرکت تنظیم کنید: ""{0}"" در تنظیمات مالیات بر ارزش افزوده امارات", +Please set a Cost Center for the Asset or set an Asset Depreciation Cost Center for the Company {},لطفاً یک مرکز هزینه برای دارایی یا یک مرکز هزینه استهلاک دارایی برای شرکت تنظیم کنید {}, +Please set a default Holiday List for Company {0},لطفاً یک فهرست تعطیلات پیش‌فرض برای شرکت {0} تنظیم کنید, +Please set an Address on the Company '%s',"لطفاً یک آدرس در شرکت ""%s"" تنظیم کنید", +Please set an Expense Account in the Items table,لطفاً یک حساب هزینه در جدول آیتم‌ها تنظیم کنید, +Please set default Exchange Gain/Loss Account in Company {},لطفاً حساب سود/زیان مبادله پیش‌فرض را در شرکت تنظیم کنید {}, +Please set default Expense Account in Company {0},لطفاً حساب هزینه پیش‌فرض را در شرکت {0} تنظیم کنید, +Please set default cost of goods sold account in company {0} for booking rounding gain and loss during stock transfer,لطفاً حساب هزینه پیش‌فرض کالاهای فروخته‌شده در شرکت {0} را برای رزرو سود و زیان در حین انتقال موجودی تنظیم کنید, +Please set filters,لطفا فیلترها را تنظیم کنید, +Please set one of the following:,لطفا یکی از موارد زیر را تنظیم کنید:, +Please set the cost center field in {0} or setup a default Cost Center for the Company.,لطفاً فیلد مرکز هزینه را در {0} تنظیم کنید یا یک مرکز هزینه پیش‌فرض برای شرکت تنظیم کنید., +Please set {0} in BOM Creator {1},لطفاً {0} را در BOM Creator {1} تنظیم کنید, +Please share this email with your support team so that they can find and fix the issue.,لطفاً این ایمیل را با تیم پشتیبانی خود به اشتراک بگذارید تا آنها بتوانند مشکل را پیدا کرده و برطرف کنند., +Please specify a {0},لطفاً یک {0} را مشخص کنید, +Please try again in an hour.,لطفا یک ساعت دیگر دوباره امتحان کنید., +Please update Repair Status.,لطفاً وضعیت تعمیر را به روز کنید., +Portal User,کاربر پورتال, +Portal Users,کاربران پورتال, +"Previous Year is not closed, please close it first",سال قبل تعطیل نیست، لطفا اول آن را ببندید, +Price ({0}),قیمت ({0}), +Price List Defaults,لیست قیمت پیش فرض, +Price Per Unit ({0}),قیمت هر واحد ({0}), +Primary Address and Contact,آدرس و مخاطب اصلی, +Primary Contact,مخاطب اصلی, +Primary Party,طرف اصلی, +Primary Role,نقش اصلی, +Print Format Builder,فرمت ساز چاپ, +Print Style,سبک چاپ, +Printing,چاپ, +Priority cannot be lesser than 1.,اولویت نمی تواند کمتر از 1 باشد., +Priority is mandatory,اولویت الزامی است, +Probability,احتمال, +Process Loss,از دست دادن فرآیند, +Process Loss Percentage cannot be greater than 100,درصد ضرر فرآیند نمی تواند بیشتر از 100 باشد, +Process Loss Qty,تعداد از دست دادن فرآیند, +Process Loss Report,گزارش از دست دادن فرآیند, +Process Loss Value,ارزش از دست دادن فرآیند, +Process Payment Reconciliation,فرآیند تطبیق پرداخت, +Process Payment Reconciliation Log,گزارش تطبیق پرداخت فرآیند, +Process Payment Reconciliation Log Allocations,تخصیص گزارش تطبیق پرداخت فرآیند, +Process Subscription,فرآیند اشتراک, +Processed BOMs,BOM های پردازش شده, +Processing Sales! Please Wait...,در حال پردازش فروش! لطفا صبر کنید..., +Produced / Received Qty,تعداد تولید / دریافت شده, +Product Price ID,شناسه قیمت محصول, +Production Plan Already Submitted,طرح تولید قبلا ارسال شده است, +Production Plan Item Reference,مرجع آیتم طرح تولید, +Production Plan Qty,مقدار برنامه تولید, +Production Plan Sub Assembly Item,آیتم زیر مونتاژ برنامه تولید, +Production Plan Sub-assembly Item,آیتم زیر مونتاژ برنامه تولید, +Production Plan Summary,خلاصه برنامه تولید, +Profile,مشخصات, +Profit and Loss Summary,خلاصه سود و زیان, +Progress,پیشرفت, +Progress (%),پیشرفت (%), +Project Progress:,پیشرفت پروژه:, +Prompt Qty,تعداد را اعلان کنید, +Prospect,مشتری بالقوه, +Prospect Lead,سرنخ مشتری بالقوه, +Prospect Opportunity,فرصت مشتری بالقوه, +Prospect Owner,مالک مشتری بالقوه, +Prospect {0} already exists,مشتری بالقوه {0} از قبل وجود دارد, +Provisional Expense Account,حساب هزینه موقت, +Purchase Order Item reference is missing in Subcontracting Receipt {0},مرجع آیتم سفارش خرید در رسید پیمانکاری فرعی وجود ندارد {0}, +Purchase Orders {0} are un-linked,سفارش‌های خرید {0} لغو پیوند هستند, +Purchase Receipt (Draft) will be auto-created on submission of Subcontracting Receipt.,رسید خرید (پیش نویس) به صورت خودکار با ارائه رسید پیمانکاری فرعی ایجاد می شود., +Purchase Receipt {0} created.,رسید خرید {0} ایجاد شد., +Purchase Value,ارزش خرید, +Purchases,خریدها, +Purposes Required,اهداف مورد نیاز, +Putaway Rule,قانون Putaway, +Putaway Rule already exists for Item {0} in Warehouse {1}.,قانون Putaway از قبل برای مورد {0} در انبار {1} وجود دارد., +Qty , تعداد, +Qty After Transaction,تعداد بعد از تراکنش, +Qty As Per BOM,مقدار طبق BOM, +Qty Change,تغییر تعداد, +Qty In Stock,تعداد موجود در انبار, +Qty Per Unit,تعداد در هر واحد, +Qty To Produce,تعداد برای تولید, +Qty and Rate,تعداد و نرخ, +Qty as Per Stock UOM,تعداد به ازای موجودی UOM, +Qty for which recursion isn't applicable.,تعداد که بازگشت برای آنها قابل اعمال نیست., +Qty in Stock UOM,تعداد موجود در انبار UOM, +Qty of Finished Goods Item should be greater than 0.,تعداد کالاهای تمام شده باید بیشتر از 0 باشد., +Qty to Be Consumed,مقدار قابل مصرف, +Qty to Build,تعداد برای ساخت, +Qty to Fetch,تعداد برای واکشی, +Qty to Produce,تعداد برای تولید, +Qualification Status,وضعیت صلاحیت, +Qualified,واجد شرایط, +Qualified By,واجد شرایط توسط, +Qualified on,واجد شرایط, +Quality Inspection Parameter,پارامتر بازرسی کیفیت, +Quality Inspection Parameter Group,گروه پارامترهای بازرسی کیفیت, +Quality Inspection Settings,تنظیمات بازرسی کیفیت, +Quality Inspection(s),بازرسی(های) کیفیت, +Quantity is required,مقدار مورد نیاز است, +"Quantity must be greater than zero, and less or equal to {0}",مقدار باید بزرگتر از صفر و کمتر یا مساوی با {0} باشد., +Quantity to Produce should be greater than zero.,مقدار تولید باید بیشتر از صفر باشد., +Quantity to Scan,مقدار برای اسکن, +Quarter {0} {1},سه ماهه {0} {1}, +Quotation Number,شماره پیش فاکتور, +Quoted Amount,مبلغ نقل شده, +Rate Difference with Purchase Invoice,تفاوت نرخ با فاکتور خرید, +Rate Section,بخش امتیاز دهی, +Rate of Stock UOM,نرخ موجودی UOM, +Ratios,نسبت ها, +Raw Material Cost Per Qty,هزینه مواد خام به ازای هر تعداد, +Raw Material Item,مورد مواد خام, +Raw Material Value,ارزش مواد خام, +Raw Materials Consumption ,مصرف مواد اولیه , +Raw Materials Warehouse,انبار مواد اولیه, +Reading Value,مقدار خواندن, +Reason for hold:,دلیل نگه داشتن:, +Rebuild Tree,درخت را بازسازی کنید, +Recalculate Incoming/Outgoing Rate,محاسبه مجدد نرخ ورودی/خروجی, +Recalculating Purchase Cost against this Project...,محاسبه مجدد هزینه خرید در مقابل این پروژه..., +Receivable/Payable Account,حساب دریافتنی/پرداختنی, +Receivable/Payable Account: {0} doesn't belong to company {1},حساب دریافتنی/پرداختنی: {0} به شرکت {1} تعلق ندارد, +Received Amount After Tax,مبلغ دریافتی پس از کسر مالیات, +Received Amount After Tax (Company Currency),مبلغ دریافتی پس از کسر مالیات (ارز شرکت), +Received Amount cannot be greater than Paid Amount,مبلغ دریافتی نمی تواند بیشتر از مبلغ پرداختی باشد, +Received Qty in Stock UOM,تعداد در انبار UOM دریافت شد, +Recent Orders,سفارش‌های اخیر, +Reconcile the Bank Transaction,تراکنش بانکی را تطبیق دهید, +Reconciled Entries,ثبت های تطبیق شده, +Reconciliation Error Log,لاگ خطای تطبیق, +Reconciliation Logs,لاگ‌های مربوط به تطبیق, +Reconciliation Progress,پیشرفت تطبیق, +Recording HTML,ضبط HTML, +Recoverable Standard Rated expenses should not be set when Reverse Charge Applicable is Y,زمانی که شارژ معکوس قابل اعمال Y است، هزینه‌های استاندارد قابل بازیافت نباید تنظیم شوند, +Recurse Every (As Per Transaction UOM),تکرار هر (بر اساس UOM تراکنش), +Recurse Over Qty cannot be less than 0,Recurse Over Qty نمی تواند کمتر از 0 باشد, +Reference Date for Early Payment Discount,تاریخ مرجع برای تخفیف پرداخت زودهنگام, +Reference Detail,جزئیات مرجع, +Reference DocType,مرجع DocType, +Reference Exchange Rate,نرخ ارز مرجع, +Reference No,شماره مرجع, +Reference number of the invoice from the previous system,شماره مرجع فاکتور از سیستم قبلی, +References {0} of type {1} had no outstanding amount left before submitting the Payment Entry. Now they have a negative outstanding amount.,مراجع {0} از نوع {1} قبل از ارسال ثبت پرداخت، مبلغ معوقه ای باقی نمانده بود. اکنون آنها یک مبلغ معوقه منفی دارند., +Refresh Google Sheet,برگه Google را بازخوانی کنید, +Refresh Plaid Link,پیوند شطرنجی را تازه کنید, +Regenerate Closing Stock Balance,بازسازی تراز موجودی اختتامیه, +Rejected Serial and Batch Bundle,باندل سریال و دسته رد شده, +Remarks Column Length,طول ستون اظهارات, +Repair,تعمیر, +Repair Asset,دارایی تعمیر, +Repair Details,جزئیات تعمیر, +Report Error,گزارش خطا, +Report Filters,گزارش فیلترها, +Report View,نمای گزارش, +Repost Accounting Ledger,بازنشر دفتر کل حسابداری, +Repost Accounting Ledger Items,بازنشر اقلام دفتر کل حسابداری, +Repost Accounting Ledger Settings,بازنشر تنظیمات دفتر کل حسابداری, +Repost Allowed Types,بازنشر انواع مجاز, +Repost Error Log,لاگ خطای ارسال مجدد, +Repost Item Valuation,ارسال مجدد ارزش گذاری مورد, +Repost Payment Ledger,ارسال مجدد دفتر کل پرداخت, +Repost Payment Ledger Items,ارسال مجدد اقلام دفتر کل پرداخت, +Repost Status,وضعیت بازنشر, +Repost has started in the background,ارسال مجدد در پس زمینه شروع شده است, +Repost in background,بازنشر در پس زمینه, +Repost started in the background,بازنشر در پس‌زمینه شروع شد, +Reposting Data File,ارسال مجدد فایل داده, +Reposting Info,بازنشر اطلاعات, +Reposting Progress,بازنشر پیشرفت, +Reposting entries created: {0},ارسال مجدد ورودی های ایجاد شده: {0}, +Reposting has been started in the background.,ارسال مجدد در پس‌زمینه آغاز شده است., +Reposting in the background.,بازنشر در پس زمینه, +Represents a Financial Year. All accounting entries and other major transactions are tracked against the Fiscal Year.,نشان دهنده یک سال مالی است. کلیه ثبت‌های حسابداری و سایر تراکنش‌های عمده در برابر سال مالی ردیابی می شوند., +Request Parameters,پارامترهای درخواست, +Request Timeout,درخواست مهلت زمانی, +Reservation Based On,رزرو بر اساس, +Reserve,ذخیره, +Reserve Stock,ذخیره موجودی, +"Reserved Qty ({0}) cannot be a fraction. To allow this, disable '{1}' in UOM {3}.","تعداد رزرو شده ({0}) نمی تواند کسری باشد. برای اجازه دادن به این کار، ""{1}"" را در UOM {3} غیرفعال کنید.", +Reserved Qty for Production Plan,تعداد رزرو شده برای طرح تولید, +Reserved Qty for Subcontract,مقدار رزرو شده برای قرارداد فرعی, +Reserved Qty should be greater than Delivered Qty.,تعداد رزرو شده باید بیشتر از تعداد تحویل شده باشد., +Reserved Serial No.,شماره سریال رزرو شده, +Reserved Stock,موجودی رزرو شده, +Reserved Stock for Batch,موجودی رزرو شده برای دسته, +Reserved for POS Transactions,برای معاملات POS رزرو شده است, +Reserved for Production,برای تولید محفوظ است, +Reserved for Production Plan,برای طرح تولید محفوظ است, +Reserved for Sub Contracting,برای پیمانکاری فرعی محفوظ است, +Reserving Stock...,رزرو موجودی..., +Reset Company Default Values,بازنشانی مقادیر پیش فرض شرکت, +Reset Plaid Link,بازنشانی پیوند Plaid, +Resolution Due,سررسید حل و فصل, +Response and Resolution,پاسخ و حل و فصل, +Restart,راه‌اندازی مجدد, +Restore Asset,بازیابی دارایی, +Restrict,محدود کردن, +Restrict Items Based On,موارد را بر اساس محدود کنید, +Result Key,کلید نتیجه, +Resume Job,رزومه کاری, +Retried,دوباره امتحان شد, +Retry,دوباره امتحان کنید, +Retry Failed Transactions,تراکنش های ناموفق را دوباره امتحان کنید, +Return Against,بازگشت در مقابل, +Return Against Subcontracting Receipt,استرداد در مقابل رسید پیمانکاری فرعی, +Return Components,برگرداندن اجزاء, +Return Issued,حواله بازگشت صادر شد, +Return Qty,تعداد برگشت, +Return Qty from Rejected Warehouse,تعداد بازگرداندن از انبار مرجوعی, +Return of Components,بازگشت اجزاء, +Returned,بازگشت, +Returned Against,بازگشت در برابر, +Returned Qty ,مقدار برگردانده شده , +Returned Qty in Stock UOM,تعداد برگردانده شده در انبار UOM, +Returned exchange rate is neither integer not float.,نرخ ارز برگشتی نه عدد صحیح است و نه شناور., +Revaluation Journals,دفترهای روزنامه تجدید ارزیابی, +Revaluation Surplus,مازاد تجدید ارزیابی, +Revenue,درآمد, +Reversal Of,معکوس شدن, +Role Allowed to Create/Edit Back-dated Transactions,نقش مجاز برای ایجاد/ویرایش تراکنش های قدیمی, +Role Allowed to Over Bill , نقش مجاز به Over Bill, +Role Allowed to Over Deliver/Receive,نقش مجاز به تحویل/دریافت بیش از حد, +Role Allowed to Override Stop Action,نقش مجاز برای لغو عمل توقف, +Role allowed to bypass Credit Limit,نقش مجاز برای دور زدن محدودیت اعتبار, +"Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity",نوع ریشه برای {0} باید یکی از دارایی، بدهی، درآمد، هزینه و حقوق صاحبان موجودی باشد., +Round Free Qty,دور تعداد رایگان, +Round Off Tax Amount,دور کردن مبلغ مالیات, +Round Tax Amount Row-wise,گرد کردن مقدار مالیات مبتنی بر ردیف, +Rounding Loss Allowance,زیان گرد کردن مجاز, +Rounding Loss Allowance should be between 0 and 1,زیان گرد کردن مجاز باید بین 0 و 1 باشد, +Rounding gain/loss Entry for Stock Transfer,گرد کردن ثبت سود/زیان برای انتقال موجودی, +Row #,ردیف #, +Row # {0}:,ردیف شماره {0}:, +Row #{0}: A reorder entry already exists for warehouse {1} with reorder type {2}.,ردیف #{0}: یک ورودی سفارش مجدد از قبل برای انبار {1} با نوع سفارش مجدد {2} وجود دارد., +Row #{0}: Acceptance Criteria Formula is incorrect.,ردیف #{0}: فرمول معیارهای پذیرش نادرست است., +Row #{0}: Acceptance Criteria Formula is required.,ردیف #{0}: فرمول معیارهای پذیرش الزامی است., +Row #{0}: Accepted Warehouse and Rejected Warehouse cannot be same,ردیف #{0}: انبار پذیرفته شده و انبار مرجوعی نمی توانند یکسان باشند, +Row #{0}: Accepted Warehouse is mandatory for the accepted Item {1},ردیف #{0}: انبار پذیرفته شده برای مورد پذیرفته شده اجباری است {1}, +Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3},ردیف #{0}: مبلغ تخصیص یافته:{1} بیشتر از مبلغ معوق است:{2} برای مدت پرداخت {3}, +Row #{0}: Amount must be a positive number,ردیف #{0}: مقدار باید یک عدد مثبت باشد, +Row #{0}: BOM is not specified for subcontracting item {0},ردیف #{0}: BOM برای قرارداد فرعی مورد {0} مشخص نشده است, +Row #{0}: Batch No {1} is already selected.,ردیف #{0}: شماره دسته {1} قبلاً انتخاب شده است., +Row #{0}: Cannot allocate more than {1} against payment term {2},ردیف #{0}: نمی توان بیش از {1} را در مقابل مدت پرداخت {2} تخصیص داد, +Row #{0}: Cannot transfer more than Required Qty {1} for Item {2} against Job Card {3},ردیف #{0}: نمی توان بیش از مقدار لازم {1} برای مورد {2} در مقابل کارت کار {3} انتقال داد, +Row #{0}: Consumed Asset {1} cannot be Draft,ردیف #{0}: دارایی مصرف شده {1} نمی تواند پیش نویس باشد, +Row #{0}: Consumed Asset {1} cannot be cancelled,ردیف #{0}: دارایی مصرف شده {1} قابل لغو نیست, +Row #{0}: Consumed Asset {1} cannot be the same as the Target Asset,ردیف #{0}: دارایی مصرف شده {1} نمی تواند با دارایی هدف یکسان باشد, +Row #{0}: Consumed Asset {1} cannot be {2},ردیف #{0}: دارایی مصرف شده {1} نمی تواند {2} باشد, +Row #{0}: Consumed Asset {1} does not belong to company {2},ردیف #{0}: دارایی مصرف شده {1} به شرکت {2} تعلق ندارد, +Row #{0}: Cumulative threshold cannot be less than Single Transaction threshold,ردیف #{0}: آستانه تجمعی نمی‌تواند کمتر از آستانه یک تراکنش باشد, +Row #{0}: Dates overlapping with other row,ردیف #{0}: تاریخ ها با ردیف دیگر همپوشانی دارند, +Row #{0}: Default BOM not found for FG Item {1},ردیف #{0}: BOM پیش‌فرض برای آیتم کالای تمام شده {1} یافت نشد, +Row #{0}: Expense Account not set for the Item {1}. {2},ردیف #{0}: حساب هزینه برای مورد {1} تنظیم نشده است. {2}, +Row #{0}: Finished Good Item Qty can not be zero,ردیف #{0}: مقدار آیتم کالای تمام شده نمی تواند صفر باشد, +Row #{0}: Finished Good Item is not specified for service item {1},ردیف #{0}: آیتم کالای تمام شده برای مورد خدماتی {1} مشخص نشده است, +Row #{0}: Finished Good Item {1} must be a sub-contracted item,ردیف #{0}: آیتم کالای تمام شده {1} باید یک آیتم قرارداد فرعی باشد, +Row #{0}: Finished Good reference is mandatory for Scrap Item {1}.,ردیف #{0}: مرجع کالای تمام شده برای ضایعات {1} اجباری است., +"Row #{0}: For {1}, you can select reference document only if account gets credited",ردیف #{0}: برای {1}، فقط در صورتی می‌توانید سند مرجع را انتخاب کنید که حساب اعتبار شود, +"Row #{0}: For {1}, you can select reference document only if account gets debited",ردیف #{0}: برای {1}، فقط در صورتی می‌توانید سند مرجع را انتخاب کنید که حساب بدهکار شود, +Row #{0}: From Date cannot be before To Date,ردیف #{0}: From Date نمی تواند قبل از To Date باشد, +Row #{0}: Item {1} does not exist,ردیف #{0}: مورد {1} وجود ندارد, +"Row #{0}: Item {1} has been picked, please reserve stock from the Pick List.",ردیف #{0}: مورد {1} انتخاب شده است، لطفاً موجودی را از فهرست انتخاب رزرو کنید., +Row #{0}: Item {1} is not a service item,ردیف #{0}: مورد {1} یک مورد خدماتی نیست, +Row #{0}: Item {1} is not a stock item,ردیف #{0}: مورد {1} یک کالای موجودی نیست, +Row #{0}: Only {1} available to reserve for the Item {2},ردیف #{0}: فقط {1} برای رزرو مورد {2} موجود است, +Row #{0}: Please select Item Code in Assembly Items,ردیف #{0}: لطفاً کد آیتم را در آیتم‌های اسمبلی انتخاب کنید, +Row #{0}: Please select the BOM No in Assembly Items,ردیف #{0}: لطفاً شماره BOM را در آیتم‌های اسمبلی انتخاب کنید, +Row #{0}: Please select the Sub Assembly Warehouse,ردیف #{0}: لطفاً انبار زیر مونتاژ را انتخاب کنید, +Row #{0}: Please update deferred revenue/expense account in item row or default account in company master,ردیف #{0}: لطفاً حساب درآمد/هزینه معوق را در ردیف آیتم یا حساب پیش‌فرض در اصلی شرکت به‌روزرسانی کنید., +Row #{0}: Qty increased by {1},ردیف #{0}: تعداد با {1} افزایش یافت, +Row #{0}: Qty must be a positive number,ردیف #{0}: تعداد باید یک عدد مثبت باشد, +Row #{0}: Qty should be less than or equal to Available Qty to Reserve (Actual Qty - Reserved Qty) {1} for Iem {2} against Batch {3} in Warehouse {4}.,ردیف #{0}: تعداد باید کمتر یا برابر با تعداد موجود برای رزرو (تعداد واقعی - تعداد رزرو شده) {1} برای Iem {2} در مقابل دسته {3} در انبار {4} باشد., +Row #{0}: Quantity to reserve for the Item {1} should be greater than 0.,ردیف #{0}: مقدار قابل رزرو برای مورد {1} باید بیشتر از 0 باشد., +Row #{0}: Rate must be same as {1}: {2} ({3} / {4}),ردیف #{0}: نرخ باید مانند {1} باشد: {2} ({3} / {4}), +Row #{0}: Received Qty must be equal to Accepted + Rejected Qty for Item {1},ردیف #{0}: تعداد دریافتی باید برابر با تعداد پذیرفته شده + رد شده برای مورد {1} باشد., +Row #{0}: Rejected Qty cannot be set for Scrap Item {1}.,سطر #{0}: تعداد رد شده را نمی توان برای ضایعات {1} تنظیم کرد., +Row #{0}: Rejected Warehouse is mandatory for the rejected Item {1},ردیف #{0}: انبار مرجوعی برای مورد رد شده اجباری است {1}, +Row #{0}: Scrap Item Qty cannot be zero,ردیف #{0}: تعداد مورد ضایعات نمی تواند صفر باشد, +Row #{0}: Serial No {1} for Item {2} is not available in {3} {4} or might be reserved in another {5}.,ردیف #{0}: شماره سریال {1} برای آیتم {2} در {3} {4} موجود نیست یا ممکن است در {5} دیگری رزرو شده باشد., +Row #{0}: Serial No {1} is already selected.,ردیف #{0}: شماره سریال {1} قبلاً انتخاب شده است., +Row #{0}: Start Time and End Time are required,ردیف #{0}: زمان شروع و زمان پایان مورد نیاز است, +Row #{0}: Start Time must be before End Time,ردیف #{0}: زمان شروع باید قبل از زمان پایان باشد, +Row #{0}: Status is mandatory,ردیف #{0}: وضعیت اجباری است, +Row #{0}: Stock cannot be reserved for Item {1} against a disabled Batch {2}.,ردیف #{0}: موجودی را نمی توان برای آیتم {1} در مقابل دسته غیرفعال شده {2} رزرو کرد., +Row #{0}: Stock cannot be reserved for a non-stock Item {1},ردیف #{0}: موجودی را نمی توان برای یک کالای غیر موجودی رزرو کرد {1}, +Row #{0}: Stock cannot be reserved in group warehouse {1}.,ردیف #{0}: موجودی در انبار گروهی {1} قابل رزرو نیست., +Row #{0}: Stock is already reserved for the Item {1}.,ردیف #{0}: موجودی قبلاً برای مورد {1} رزرو شده است., +Row #{0}: Stock is reserved for item {1} in warehouse {2}.,ردیف #{0}: موجودی برای کالای {1} در انبار {2} رزرو شده است., +Row #{0}: Stock not available to reserve for Item {1} against Batch {2} in Warehouse {3}.,ردیف #{0}: موجودی برای رزرو مورد {1} در مقابل دسته {2} در انبار {3} موجود نیست., +Row #{0}: Stock not available to reserve for the Item {1} in Warehouse {2}.,ردیف #{0}: موجودی برای رزرو مورد {1} در انبار {2} موجود نیست., +Row #{0}: You cannot use the inventory dimension '{1}' in Stock Reconciliation to modify the quantity or valuation rate. Stock reconciliation with inventory dimensions is intended solely for performing opening entries.,ردیف #{0}: نمی‌توانید از بعد موجودی «{1}» در تطبیق موجودی برای تغییر مقدار یا نرخ ارزیابی استفاده کنید. تطبیق موجودی با ابعاد موجودی صرفاً برای انجام ورودی های افتتاحیه در نظر گرفته شده است., +Row #{0}: You must select an Asset for Item {1}.,ردیف #{0}: باید یک دارایی برای مورد {1} انتخاب کنید., +Row #{0}: {1} is not a valid reading field. Please refer to the field description.,ردیف #{0}: {1} یک فیلد خواندنی معتبر نیست. لطفا به توضیحات فیلد مراجعه کنید., +Row #{0}: {1} of {2} should be {3}. Please update the {1} or select a different account.,ردیف #{0}: {1} از {2} باید {3} باشد. لطفاً {1} را به روز کنید یا حساب دیگری را انتخاب کنید., +Row #{1}: Warehouse is mandatory for stock Item {0},ردیف #{1}: انبار برای کالای موجودی {0} اجباری است, +Row #{}: Finance Book should not be empty since you're using multiple.,ردیف #{}: کتاب مالی نباید خالی باشد زیرا از چندگانه استفاده می کنید., +Row #{}: Please use a different Finance Book.,ردیف #{}: لطفاً از کتاب مالی دیگری استفاده کنید., +Row #{}: item {} has been picked already.,ردیف #{}: مورد {} قبلاً انتخاب شده است., +Row #{}: {} {} doesn't belong to Company {}. Please select valid {}.,ردیف #{}: {} {} به شرکت {} تعلق ندارد. لطفاً {} معتبر را انتخاب کنید., +Row No {0}: Warehouse is required. Please set a Default Warehouse for Item {1} and Company {2},ردیف شماره {0}: انبار مورد نیاز است. لطفاً یک انبار پیش فرض برای مورد {1} و شرکت {2} تنظیم کنید, +Row Number,شماره ردیف, +Row {0},ردیف {0}, +"Row {0} picked quantity is less than the required quantity, additional {1} {2} required.",مقدار انتخابی ردیف {0} کمتر از مقدار مورد نیاز است، {1} {2} اضافی مورد نیاز است., +Row {0}# Item {1} cannot be transferred more than {2} against {3} {4},ردیف {0}# مورد {1} را نمی توان بیش از {2} در برابر {3} {4} منتقل کرد, +Row {0}# Item {1} not found in 'Raw Materials Supplied' table in {2} {3},ردیف {0}# مورد {1} در جدول «مواد خام عرضه شده» در {2} {3} یافت نشد, +Row {0}: Accepted Qty and Rejected Qty can't be zero at the same time.,ردیف {0}: تعداد پذیرفته شده و تعداد رد شده نمی توانند همزمان صفر باشند., +Row {0}: Account {1} and Party Type {2} have different account types,ردیف {0}: حساب {1} و نوع طرف {2} انواع مختلف حساب دارند, +Row {0}: Allocated amount {1} must be less than or equal to invoice outstanding amount {2},ردیف {0}: مبلغ تخصیص یافته {1} باید کمتر یا برابر با مبلغ معوق فاکتور {2} باشد., +Row {0}: Allocated amount {1} must be less than or equal to remaining payment amount {2},ردیف {0}: مبلغ تخصیص یافته {1} باید کمتر یا مساوی با مبلغ پرداخت باقی مانده باشد {2}, +Row {0}: Both Debit and Credit values cannot be zero,ردیف {0}: هر دو مقدار بدهی و اعتبار نمی توانند صفر باشند, +Row {0}: Cost Center {1} does not belong to Company {2},ردیف {0}: مرکز هزینه {1} به شرکت {2} تعلق ندارد, +Row {0}: Either Delivery Note Item or Packed Item reference is mandatory.,ردیف {0}: مرجع مورد یادداشت تحویل یا کالای بسته بندی شده اجباری است., +Row {0}: Expense Head changed to {1} as no Purchase Receipt is created against Item {2}.,ردیف {0}: سر هزینه به {1} تغییر کرد زیرا هیچ رسید خریدی در برابر مورد {2} ایجاد نشد., +Row {0}: Expense Head changed to {1} because account {2} is not linked to warehouse {3} or it is not the default inventory account,ردیف {0}: سر هزینه به {1} تغییر کرد زیرا حساب {2} به انبار {3} مرتبط نیست یا حساب موجودی پیش‌فرض نیست, +Row {0}: Expense Head changed to {1} because expense is booked against this account in Purchase Receipt {2},ردیف {0}: سر هزینه به {1} تغییر کرد زیرا هزینه در قبض خرید {2} در مقابل این حساب رزرو شده است., +Row {0}: From Warehouse is mandatory for internal transfers,ردیف {0}: از انبار برای نقل و انتقالات داخلی اجباری است, +Row {0}: Item Tax template updated as per validity and rate applied,ردیف {0}: الگوی مالیات آیتم بر اساس اعتبار و نرخ اعمال شده به روز شد, +Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer,ردیف {0}: نرخ اقلام براساس نرخ ارزیابی به‌روزرسانی شده است، زیرا یک انتقال داخلی موجودی است, +Row {0}: Item {1} must be a stock item.,ردیف {0}: مورد {1} باید یک کالای موجودی باشد., +Row {0}: Item {1} must be a subcontracted item.,ردیف {0}: مورد {1} باید یک آیتم قرارداد فرعی باشد., +Row {0}: Packed Qty must be equal to {1} Qty.,ردیف {0}: تعداد بسته بندی شده باید برابر با {1} تعداد باشد., +Row {0}: Packing Slip is already created for Item {1}.,ردیف {0}: برگه بسته بندی قبلاً برای مورد {1} ایجاد شده است., +Row {0}: Payment Term is mandatory,ردیف {0}: مدت پرداخت اجباری است, +Row {0}: Please provide a valid Delivery Note Item or Packed Item reference.,ردیف {0}: لطفاً یک مورد یادداشت تحویل معتبر یا مرجع کالای بسته بندی شده ارائه دهید., +Row {0}: Please select a BOM for Item {1}.,ردیف {0}: لطفاً یک BOM برای مورد {1} انتخاب کنید., +Row {0}: Please select an active BOM for Item {1}.,ردیف {0}: لطفاً یک BOM فعال برای مورد {1} انتخاب کنید., +Row {0}: Please select an valid BOM for Item {1}.,ردیف {0}: لطفاً یک BOM معتبر برای مورد {1} انتخاب کنید., +Row {0}: Project must be same as the one set in the Timesheet: {1}.,ردیف {0}: پروژه باید مانند آنچه در صفحه زمان تنظیم شده است: {1} باشد., +Row {0}: Purchase Invoice {1} has no stock impact.,ردیف {0}: فاکتور خرید {1} تأثیری بر موجودی ندارد., +Row {0}: Qty cannot be greater than {1} for the Item {2}.,ردیف {0}: تعداد نمی‌تواند بیشتر از {1} برای مورد {2} باشد., +Row {0}: Qty in Stock UOM can not be zero.,ردیف {0}: تعداد موجودی UOM در انبار نمی تواند صفر باشد., +Row {0}: Qty must be greater than 0.,ردیف {0}: تعداد باید بیشتر از 0 باشد., +Row {0}: Shift cannot be changed since the depreciation has already been processed,ردیف {0}: Shift را نمی توان تغییر داد زیرا استهلاک قبلاً پردازش شده است, +Row {0}: Target Warehouse is mandatory for internal transfers,ردیف {0}: انبار هدف برای نقل و انتقالات داخلی اجباری است, +"Row {0}: To set {1} periodicity, difference between from and to date must be greater than or equal to {2}",ردیف {0}: برای تنظیم تناوب {1}، تفاوت بین تاریخ و تاریخ باید بزرگتر یا مساوی با {2} باشد., +Row {0}: {1} account already applied for Accounting Dimension {2},ردیف {0}: حساب {1} قبلاً برای بعد حسابداری {2} اعمال شده است, +Row {0}: {1} {2} cannot be same as {3} (Party Account) {4},ردیف {0}: {1} {2} نمی‌تواند مانند {3} (حساب طرف) {4}, +Row({0}): Outstanding Amount cannot be greater than actual Outstanding Amount {1} in {2},ردیف ({0}): مبلغ معوق نمی تواند بیشتر از مبلغ معوقه واقعی {1} در {2} باشد., +Rows with Same Account heads will be merged on Ledger,ردیف هایی با سرهای حساب یکسان در دفتر کل ادغام می شوند, +Rows: {0} have 'Payment Entry' as reference_type. This should not be set manually.,"ردیف‌ها: {0} دارای ""ثبت پرداخت"" به عنوان reference_type هستند. این نباید به صورت دستی تنظیم شود.", +Rows: {0} in {1} section are Invalid. Reference Name should point to a valid Payment Entry or Journal Entry.,ردیف‌ها: {0} در بخش {1} نامعتبر است. نام مرجع باید به یک ثبت پرداخت معتبر یا ثبت دفتر روزنامه اشاره کند., +Run parallel job cards in a workstation,اجرای موازی کارت های در یک ایستگاه کاری, +Running,در حال اجرا, +SCO Supplied Item,مورد عرضه شده SCO, +SLA Fulfilled On,SLA تکمیل شد, +SLA Fulfilled On Status,SLA در وضعیت تکمیل شد, +SLA Paused On,SLA متوقف شد, +SLA will be applied if {1} is set as {2}{3},اگر {1} به عنوان {2}{3} تنظیم شود، SLA اعمال خواهد شد, +SLA will be applied on every {0},SLA در هر {0} اعمال خواهد شد, +SMS Settings,تنظیمات پیامک, +Salary Currency,ارز حقوق و دستمزد, +Sales Invoice {0} must be deleted before cancelling this Sales Order,فاکتور فروش {0} باید قبل از لغو این سفارش فروش حذف شود, +Sales Order Packed Item,سفارش فروش کالای بسته بندی شده, +Sales Order Reference,مرجع سفارش فروش, +Sales Order Status,وضعیت سفارش فروش, +"Sales Order {0} already exists against Customer's Purchase Order {1}. To allow multiple Sales Orders, Enable {2} in {3}",سفارش فروش {0} در مقابل سفارش خرید مشتری {1} وجود دارد. برای مجاز کردن چندین سفارش فروش، {2} را در {3} فعال کنید, +Sales Partner , شریک فروش, +Sales Partner Item,مورد شریک فروش, +Sales Pipeline Analytics,تجزیه و تحلیل خط لوله فروش, +Sales Update Frequency in Company and Project,فرکانس به روز رسانی فروش در شرکت و پروژه, +Sales Value,ارزش فروش, +Salvage Value Percentage,درصد ارزش نجات, +Same item and warehouse combination already entered.,همان کالا و ترکیب انبار قبلا وارد شده است., +Savings,پس انداز, +Scan Batch No,اسکن شماره دسته, +Scan Mode,حالت اسکن, +Scan Serial No,اسکن شماره سریال, +Scan barcode for item {0},اسکن بارکد برای مورد {0}, +"Scan mode enabled, existing quantity will not be fetched.",حالت اسکن فعال است، مقدار موجود واکشی نخواهد شد., +Scanned Quantity,مقدار اسکن شده, +Scheduled Time Logs,گزارش های زمان برنامه ریزی شده, +Scheduler is Inactive. Can't trigger job now.,زمان‌بند غیرفعال است. اکنون نمی توان کار را آغاز کرد., +Scheduler is Inactive. Can't trigger jobs now.,زمان‌بند غیرفعال است. اکنون نمی توان کارها را آغاز کرد., +Scheduler is inactive. Cannot enqueue job.,زمانبند غیرفعال است. نمی توان کار را در نوبت گذاشت., +Scheduler is inactive. Cannot merge accounts.,زمانبند غیرفعال است. نمی توان حساب ها را ادغام کرد., +Scheduling,برنامه ریزی, +Scrap & Process Loss,ضایعات و از دست دادن فرآیند, +Scrap Asset,دارایی قراضه, +Scrap Cost Per Qty,هزینه قراضه در هر تعداد, +Scrap Item Code,کد آیتم ضایعات, +Scrap Item Name,نام آیتم ضایعات, +"Search by item code, serial number or barcode",جستجو بر اساس کد آیتم، شماره سریال یا بارکد, +Secondary Party,طرف ثانویه, +Secondary Role,نقش ثانویه, +Segregate Serial / Batch Bundle,جداسازی باندل سریال / دسته, +Select Accounting Dimension.,بعد حسابداری را انتخاب کنید., +Select Alternative Items for Sales Order,آیتم‌های جایگزین را برای سفارش فروش انتخاب کنید, +Select Batch No,شماره دسته را انتخاب کنید, +Select Corrective Operation,عملیات اصلاحی را انتخاب کنید, +Select Date of Birth. This will validate Employees age and prevent hiring of under-age staff.,تاریخ تولد را انتخاب کنید. این امر سن کارکنان را تأیید می کند و از استخدام کارکنان زیر سن جلوگیری می کند., +"Select Date of joining. It will have impact on the first salary calculation, Leave allocation on pro-rata bases.",تاریخ عضویت را انتخاب کنید. در اولین محاسبه حقوق، تخصیص مرخصی به نسبت، تاثیر خواهد داشت., +Select Dimension,Dimension را انتخاب کنید, +Select Finished Good,Finished Good را انتخاب کنید, +Select Items for Quality Inspection,موارد را برای بازرسی کیفیت انتخاب کنید, +Select Serial No,شماره سریال را انتخاب کنید, +Select Serial and Batch,سریال و دسته را انتخاب کنید, +Select Time,زمان را انتخاب کنید, +Select Vouchers to Match,کوپن‌ها را برای مطابقت انتخاب کنید, +Select Warehouses to get Stock for Materials Planning,برای بدست آوردن انبار برای برنامه ریزی مواد، انبارها را انتخاب کنید, +Select a Company this Employee belongs to.,شرکتی را انتخاب کنید که این کارمند به آن تعلق دارد., +Select a Customer,یک مشتری انتخاب کنید, +Select an Item Group.,یک گروه آیتم را انتخاب کنید., +Select an item from each set to be used in the Sales Order.,از هر مجموعه یک مورد را برای استفاده در سفارش فروش انتخاب کنید., +Select the Default Workstation where the Operation will be performed. This will be fetched in BOMs and Work Orders.,ایستگاه کاری پیش‌فرض را که در آن عملیات انجام می‌شود، انتخاب کنید. این در BOM ها و دستور کارها واکشی می شود., +Select the Item to be manufactured.,موردی را که باید تولید شود انتخاب کنید., +"Select the Item to be manufactured. The Item name, UoM, Company, and Currency will be fetched automatically.",موردی را که باید تولید شود انتخاب کنید. نام مورد، UoM، شرکت و ارز به طور خودکار واکشی می شود., +Select the Warehouse,انبار را انتخاب کنید, +Select the date and your timezone,تاریخ و منطقه زمانی خود را انتخاب کنید, +Select the raw materials (Items) required to manufacture the Item,مواد خام (آیتم‌ها) مورد نیاز برای تولید آیتم را انتخاب کنید, +Select your weekly off day,روز تعطیل هفتگی خود را انتخاب کنید, +Selected Vouchers,کوپن های انتخاب شده, +Selected date is,تاریخ انتخاب شده است, +Selected document must be in submitted state,سند انتخاب شده باید در حالت ارسال شده باشد, +Self delivery,تحویل خود, +Sell Asset,فروش دارایی, +Send Attached Files,ارسال فایل های پیوست, +Send Document Print,ارسال سند چاپ, +Send Emails,ارسال ایمیل, +Sequential,متوالی, +Serial & Batch Item,آیتم سریال و دسته ای, +Serial & Batch Item Settings,تنظیمات آیتم سریال و دسته ای, +Serial / Batch Bundle,باندل سریال / دسته, +Serial / Batch Bundle Missing,باندل سریال / دسته جا افتاده, +Serial / Batch No,شماره سریال / دسته, +Serial / Batch Nos,شماره های سریال / دسته ای, +Serial No Ledger,دفتر کل شماره سریال, +Serial No Range,محدوده شماره سریال, +Serial No and Batch for Finished Good,شماره سریال و دسته ای برای Finished Good, +Serial No is mandatory,شماره سریال اجباری است, +Serial No {0} already exists,شماره سریال {0} از قبل وجود دارد, +Serial No {0} already scanned,شماره سریال {0} قبلاً اسکن شده است, +Serial No {0} does not exists,شماره سریال {0} وجود ندارد, +Serial No {0} is already added,شماره سریال {0} قبلاً اضافه شده است, +Serial Nos,شماره های سریال, +Serial Nos / Batch Nos,شماره های سریال / شماره های دسته ای, +Serial Nos Mismatch,عدم تطابق شماره های سریال, +Serial Nos are created successfully,شماره های سریال با موفقیت ایجاد شد, +"Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding.",شماره های سریال در ورودی های رزرو موجودی رزرو شده اند، قبل از ادامه باید آنها را لغو رزرو کنید., +Serial and Batch,سریال و دسته, +Serial and Batch Bundle,باندل سریال و دسته, +Serial and Batch Bundle created,باندل سریال و دسته ایجاد شد, +Serial and Batch Bundle updated,باندل سریال و دسته به روز شد, +Serial and Batch Details,جزئیات سریال و دسته, +Serial and Batch Entry,ثبت سریال و دسته, +Serial and Batch No,شماره سریال و دسته, +Serial and Batch Nos,شماره سریال و دسته, +Serial and Batch Nos will be auto-reserved based on Pick Serial / Batch Based On,شماره های سریال و دسته بر اساس انتخاب سریال / دسته بر اساس به صورت خودکار رزرو می شوند, +Serial and Batch Reservation,رزرو سریال و دسته, +Serial and Batch Summary,خلاصه سریال و دسته ای, +Service Cost Per Qty,هزینه خدمات در هر تعداد, +Service Expense Total Amount,هزینه خدمات کل مبلغ, +Service Expenses,هزینه های خدمات, +Service Item,آیتم خدمات, +Service Item Qty,تعداد مورد خدمات, +Service Item Qty / Finished Good Qty,تعداد آیتم خدمات / تعداد کالای تمام شده, +Service Item UOM,مورد خدمات UOM, +Service Item {0} is disabled.,مورد سرویس {0} غیرفعال است., +Service Item {0} must be a non-stock item.,مورد سرویس {0} باید یک کالای غیر موجودی باشد., +Service Items,آیتم‌های خدماتی, +Service Level Agreement for {0} {1} already exists.,قرارداد سطح سرویس برای {0} {1} از قبل وجود دارد., +Service Level Name,نام سطح خدمات, +Service Provider,ارائه دهنده خدمات, +Set Default Supplier,تامین کننده پیش فرض را تنظیم کنید, +Set From Warehouse,مجموعه از انبار, +Set Landed Cost Based on Purchase Invoice Rate,تنظیم هزینه تمام شده تا درب انبار بر اساس نرخ فاکتور خرید, +Set Loyalty Program,تنظیم برنامه وفاداری, +Set Operating Cost / Scrape Items From Sub-assemblies,تنظیم هزینه عملیاتی / آیتم‌های ضایعات از زیر مونتاژها, +Set Operating Cost Based On BOM Quantity,هزینه عملیاتی را بر اساس مقدار BOM تنظیم کنید, +Set Process Loss Item Quantity,مقدار مورد از دست دادن فرآیند را تنظیم کنید, +Set Project Status,تنظیم وضعیت پروژه, +Set Quantity,تنظیم مقدار, +Set Response Time for Priority {0} in row {1}.,زمان پاسخ را برای اولویت {0} در ردیف {1} تنظیم کنید., +Set Valuation Rate Based on Source Warehouse,نرخ ارزش گذاری را بر اساس انبار منبع تنظیم کنید, +Set Warehouse,مجموعه انبار, +Set default {0} account for non stock items,تنظیم حساب پیش‌فرض {0} را برای آیتم‌های غیر موجودی, +Set fieldname from which you want to fetch the data from the parent form.,نام فیلدی را که می‌خواهید داده‌ها را از فرم والد دریافت کنید، تنظیم کنید., +Set quantity of process loss item:,تنظیم مقدار مورد از دست دادن فرآیند:, +Set the Planned Start Date (an Estimated Date at which you want the Production to begin),تاریخ شروع برنامه ریزی شده را تنظیم کنید (تاریخ تخمینی که در آن می خواهید تولید شروع شود), +Set the status manually.,وضعیت را به صورت دستی تنظیم کنید., +Set {0} in asset category {1} for company {2},تنظیم {0} در دسته دارایی {1} برای شرکت {2}, +Sets 'Accepted Warehouse' in each row of the Items table.,انبار پذیرفته شده را در هر ردیف از جدول آیتم ها تنظیم می کند., +Sets 'Rejected Warehouse' in each row of the Items table.,انبار مرجوعی را در هر ردیف از جدول آیتم ها تنظیم می کند., +Sets 'Reserve Warehouse' in each row of the Supplied Items table.,انبار ذخیره را در هر ردیف از جدول اقلام عرضه شده تنظیم می کند., +Setting Item Locations...,تنظیم مکان مورد..., +Setting the account as a Company Account is necessary for Bank Reconciliation,تنظیم حساب به عنوان حساب شرکت برای تسویه حساب بانکی ضروری است, +Setting {} is required,تنظیم {} مورد نیاز است, +Setup your organization,سازمان خود را راه‌اندازی کنید, +Shift,تغییر مکان, +Shift Factor,ضریب تغییر, +Shift Name,نام شیفت, +Shipment,حمل و نقل, +Shipment Amount,مبلغ حمل و نقل, +Shipment Delivery Note,یادداشت تحویل حمل و نقل, +Shipment ID,شناسه حمل و نقل, +Shipment Information,اطلاعات حمل و نقل, +Shipment Parcel,بسته حمل و نقل, +Shipment Parcel Template,قالب بسته حمل و نقل, +Shipment Type,نوع حمل و نقل, +Shipment details,جزئیات حمل و نقل, +Shipping Address Details,جزئیات آدرس حمل و نقل, +Shipping Address Template,الگوی آدرس حمل و نقل, +Show Balances in Chart Of Accounts,نمایش موجودی در نمودار حساب, +Show Barcode Field in Stock Transactions,نمایش فیلد بارکد در معاملات موجودی, +Show Dimension Wise Stock,نمایش موجودی بر اساس ابعاد, +Show Disabled Warehouses,نمایش انبارهای غیرفعال, +Show Failed Logs,نمایش لاگ های ناموفق, +Show GL Balance,نمایش تراز دفتر کل, +Show Item Name,نمایش نام آیتم, +Show Ledger View,نمایش نمای دفتر کل, +Show Net Values in Party Account,نمایش ارزش خالص در حساب طرف, +Show Pay Button in Purchase Order Portal,نمایش دکمه پرداخت در پورتال سفارش خرید, +Show Preview,نمایش پیش نمایش, +Show Remarks,نمایش اظهارات, +Show Taxes as Table in Print,نمایش مالیات به عنوان جدول در چاپ, +Show net values in opening and closing columns,نمایش مقادیر خالص در ستون افتتاحیه و اختتامیه, +Show only the Immediate Upcoming Term,فقط عبارت فوری آینده را نشان دهید, +Show pending entries,نمایش ورودی های معلق, +Show with upcoming revenue/expense,نمایش با درآمد/هزینه آتی, +"Simple Python Expression, Example: doc.status == 'Open' and doc.issue_type == 'Bug'",عبارت ساده پایتون، مثال: doc.status == 'Open' و doc.issue_type == 'Bug', +Simultaneous,همزمان, +Skip Available Sub Assembly Items,صرف نظر از آیتم‌های زیر مونتاژ موجود, +Skipped,رد شد, +Skipping Tax Withholding Category {0} as there is no associated account set for Company {1} in it.,رد شدن از دسته مالیات کسر مالیات {0} زیرا هیچ حساب مرتبطی برای شرکت {1} در آن تنظیم نشده است., +"Skipping {0} of {1}, {2}",پرش از {0} از {1}، {2}, +Sold by,فروخته شده توسط, +Something went wrong please try again,اشتباهی رخ داده لطفا دوباره تلاش کنید, +Source Exchange Rate,نرخ مبادله منبع, +Source Fieldname,نام فیلد منبع, +South Africa VAT Account,حساب مالیات بر ارزش افزوده آفریقای جنوبی, +South Africa VAT Settings,تنظیمات مالیات بر ارزش افزوده آفریقای جنوبی, +Spacer,اسپیسر, +Split Asset,تقسیم دارایی, +Split Early Payment Discount Loss into Income and Tax Loss,زیان تخفیف پرداخت زودهنگام را به درآمد و ضرر مالیات تقسیم کنید, +Split From,تقسیم از, +Split Qty,تقسیم تعداد, +Split qty cannot be grater than or equal to asset qty,مقدار تقسیم نمی‌تواند بیشتر یا مساوی تعداد دارایی باشد, +Splitting {0} {1} into {2} rows as per Payment Terms,تقسیم {0} {1} به ردیف‌های {2} طبق شرایط پرداخت, +Stage,صحنه, +Stale Days should start from 1.,روزهای قدیمی باید از 1 شروع شود., +Standard Description,شرح استاندارد, +Standard Rated Expenses,هزینه های رتبه بندی استاندارد, +"Standard Terms and Conditions that can be added to Sales and Purchases. Examples: Validity of the offer, Payment Terms, Safety and Usage, etc.",شرایط و ضوابط استاندارد که می تواند به خرید و فروش اضافه شود. مثال: اعتبار پیشنهاد، شرایط پرداخت، ایمنی و استفاده و غیره., +Standard rated supplies in {0},منابع دارای رتبه استاندارد در {0}, +Start / Resume,شروع / از سرگیری, +Start Date should be lower than End Date,تاریخ شروع باید کمتر از تاریخ پایان باشد, +Start Deletion,شروع حذف, +Start Import,شروع درون‌بُرد, +Start Job,شروع کار, +Start Merge,ادغام را شروع کنید, +Start Reposting,بازنشر را شروع کنید, +Start Time can't be greater than or equal to End Time for {0}.,زمان شروع نمی تواند بزرگتر یا مساوی با زمان پایان برای {0} باشد., +Started a background job to create {1} {0},یک کار پس‌زمینه برای ایجاد {1} {0} شروع کرد, +Status Details,جزئیات وضعیت, +Status Illustration,مصور سازی وضعیت, +Status set to rejected as there are one or more rejected readings.,وضعیت رد شد زیرا یک یا چند قرائت رد شده وجود دارد., +Stock Closing,اختتامیه موجودی, +Stock Consumed During Repair,موجودی مصرف شده در حین تعمیر, +Stock Consumption Details,جزئیات مصرف موجودی, +Stock Entries already created for Work Order {0}: {1},ثبت های موجودی قبلاً برای دستور کار {0} ایجاد شده‌اند: {1}, +Stock Ledger Invariant Check,چک ثابت دفتر کل موجودی, +Stock Ledger Variance,واریانس دفتر کل موجودی, +Stock Movement,جابه‌جایی موجودی, +Stock Planning,برنامه ریزی موجودی, +Stock Reposting Settings,تنظیمات ارسال مجدد موجودی, +Stock Reservation,رزرو موجودی, +Stock Reservation Entries Cancelled,ورودی های رزرو موجودی لغو شد, +Stock Reservation Entries Created,نوشته های رزرو موجودی ایجاد شد, +Stock Reservation Entry,ثبت رزرو موجودی, +Stock Reservation Entry cannot be updated as it has been delivered.,ورودی رزرو موجودی را نمی توان به دلیل تحویل گرفتن به روز کرد., +"Stock Reservation Entry created against a Pick List cannot be updated. If you need to make changes, we recommend canceling the existing entry and creating a new one.",ثبت رزرو موجودی ایجاد شده در برابر فهرست انتخابی نمی تواند به روز شود. اگر نیاز به ایجاد تغییرات دارید، توصیه می کنیم ثبت موجود را لغو کنید و یک ثبت جدید ایجاد کنید., +Stock Reservation Warehouse Mismatch,عدم تطابق انبار رزرو انبار, +Stock Reservation can only be created against {0}.,رزرو موجودی فقط می تواند در مقابل {0} ایجاد شود., +Stock Reserved Qty (in Stock UOM),تعداد موجودی رزرو شده (در انبار UOM), +Stock Transactions Settings,تنظیمات معاملات موجودی, +Stock UOM Quantity,مقدار موجودی UOM, +Stock Unreservation,عدم رزرو موجودی, +Stock Validations,اعتبارسنجی موجودی, +Stock cannot be reserved in group warehouse {0}.,موجودی در انبار گروهی {0} قابل رزرو نیست., +Stock cannot be reserved in the group warehouse {0}.,موجودی در انبار گروهی {0} قابل رزرو نیست., +Stock not available for Item {0} in Warehouse {1}.,موجودی برای کالای {0} در انبار {1} موجود نیست., +Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2} {3}.,مقدار موجودی برای کد مورد کافی نیست: {0} در انبار {1}. مقدار موجود {2} {3}., +Stock transactions that are older than the mentioned days cannot be modified.,معاملات موجودی با قدمت بیشتر از روزهای مذکور قابل تغییر نمی باشد., +Stock/Accounts can not be frozen as processing of backdated entries is going on. Please try again later.,موجودی/حساب‌ها را نمی‌توان مسدود کرد زیرا پردازش ورودی‌های به‌تاریخ در حال انجام است. لطفاً بعداً دوباره امتحان کنید., +Sub Assemblies & Raw Materials,زیر مونتاژها و مواد اولیه, +Sub Assembly Item,آیتم زیر مونتاژ, +Sub Assembly Item Code,کد آیتم‌های زیر مونتاژ, +Sub Assembly Items,آیتم‌های زیر مونتاژ, +Sub Assembly Warehouse,انبار زیر مونتاژ, +Sub Operation,عملیات فرعی, +Sub Operations,عملیات فرعی, +Subcontract BOM,قرارداد فرعی BOM, +Subcontract Order,سفارش قرارداد فرعی, +Subcontract Order Summary,خلاصه سفارش قرارداد فرعی, +Subcontract Return,بازگشت قرارداد فرعی, +Subcontracting BOM,پیمانکاری فرعی BOM, +Subcontracting Order,سفارش پیمانکاری فرعی, +Subcontracting Order (Draft) will be auto-created on submission of Purchase Order.,سفارش پیمانکاری فرعی (پیش نویس) با ارسال سفارش خرید به صورت خودکار ایجاد می شود., +Subcontracting Order Item,مورد سفارش پیمانکاری فرعی, +Subcontracting Order Service Item,آیتم خدمات سفارش قرارداد فرعی, +Subcontracting Order Supplied Item,اقلام عرضه شده سفارش پیمانکاری فرعی, +Subcontracting Order {0} created.,سفارش قرارداد فرعی {0} ایجاد شد., +Subcontracting Purchase Order,سفارش خرید پیمانکاری فرعی, +Subcontracting Receipt,رسید پیمانکاری فرعی, +Subcontracting Receipt Item,آیتم رسید پیمانکاری, +Subcontracting Receipt Supplied Item,آیتم عرضه شده رسید پیمانکاری فرعی, +Subcontracting Settings,تنظیمات پیمانکاری فرعی, +Subdivision,زیر مجموعه, +Submit Action Failed,اقدام ارسال نشد, +Submit After Import,ارسال پس از درون‌بُرد, +Submit ERR Journals?,دفترهای روزنامه ERR ارسال شود؟, +Submit Generated Invoices,فاکتورهای تولید شده را ارسال کنید, +Succeeded,موفق شد, +Succeeded Entries,ورودی های موفق, +"Successfully changed Stock UOM, please redefine conversion factors for new UOM.",UOM موجودی با موفقیت تغییر کرد، لطفاً فاکتورهای تبدیل را برای UOM جدید دوباره تعریف کنید., +Successfully imported {0},{0} با موفقیت درون‌بُرد شد, +"Successfully imported {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.",{0} رکورد از {1} با موفقیت درون‌بُرد شد. روی Export Errored Rows کلیک کنید، خطاها را برطرف کرده و دوباره وارد کنید., +Successfully imported {0} record.,{0} رکورد با موفقیت درون‌بُرد شد., +"Successfully imported {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.",{0} رکورد از {1} با موفقیت درون‌بُرد شد. روی Export Errored Rows کلیک کنید، خطاها را برطرف کرده و دوباره وارد کنید., +Successfully imported {0} records.,{0} رکورد با موفقیت درون‌بُرد شد., +Successfully linked to Customer,با موفقیت به مشتری پیوند داده شد, +Successfully linked to Supplier,با موفقیت به تامین کننده پیوند داده شد, +Successfully merged {0} out of {1}.,{0} از {1} با موفقیت ادغام شد., +Successfully updated {0},با موفقیت به روز شد {0}, +"Successfully updated {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.",{0} رکورد از {1} با موفقیت به روز شد. روی Export Errored Rows کلیک کنید، خطاها را برطرف کرده و دوباره وارد کنید., +Successfully updated {0} record.,رکورد {0} با موفقیت به روز شد., +"Successfully updated {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.",{0} رکورد از {1} با موفقیت به روز شد. روی Export Errored Rows کلیک کنید، خطاها را برطرف کرده و دوباره وارد کنید., +Successfully updated {0} records.,رکورد {0} با موفقیت به روز شد., +Sum of Repair Cost and Value of Consumed Stock Items.,مجموع هزینه تعمیر و ارزش آیتم‌های موجودی مصرف شده., +Supplied Item,مورد عرضه شده, +Supplier Address Details,جزئیات آدرس تامین کننده, +Supplier Contact,تماس با تامین کننده, +Supplier Group Item,مورد گروه تامین کننده, +Supplier Info,اطلاعات تامین کننده, +Supplier Invoice,فاکتور تامین کننده, +Supplier Item,مورد تامین کننده, +Supplier Portal Users,کاربران پورتال تامین کننده, +Supplier Primary Address,آدرس اصلی تامین کننده, +Supplier Primary Contact,تماس اصلی تامین کننده, +Supplier Warehouse mandatory for sub-contracted {0},انبار تامین کننده برای قراردادهای فرعی {0} اجباری است, +Supplies subject to the reverse charge provision,لوازم مشمول ارائه شارژ معکوس, +Sync Now,اکنون همگام سازی کنید, +Sync Started,همگام سازی شروع شد, +System Settings,تنظیمات سیستم, +System will automatically create the serial numbers / batch for the Finished Good on submission of work order,سیستم به طور خودکار شماره سریال / دسته ای را برای کالای تمام شده در هنگام ارسال سفارش ایجاد می کند, +System will not check over billing since amount for Item {0} in {1} is zero,سیستم صورتحساب را بررسی نمی‌کند زیرا مبلغ مورد {0} در {1} صفر است, +TDS Payable,TDS قابل پرداخت, +Target Asset,دارایی هدف, +Target Asset Location,مکان دارایی مورد نظر, +Target Asset {0} cannot be cancelled,دارایی هدف {0} قابل لغو نیست, +Target Asset {0} cannot be submitted,دارایی هدف {0} قابل ارسال نیست, +Target Asset {0} cannot be {1},دارایی هدف {0} نمی تواند {1} باشد, +Target Asset {0} does not belong to company {1},دارایی هدف {0} به شرکت {1} تعلق ندارد, +Target Asset {0} needs to be composite asset,دارایی هدف {0} باید دارایی ترکیبی باشد, +Target Batch No,شماره دسته هدف, +Target Exchange Rate,نرخ ارز هدف, +Target Fieldname (Stock Ledger Entry),نام فیلد هدف (ثبت دفتر کل موجودی), +Target Fixed Asset Account,حساب دارایی ثابت هدف, +Target Has Batch No,هدف دارای Batch No, +Target Has Serial No,هدف دارای شماره سریال, +Target Incoming Rate,نرخ ورودی هدف, +Target Is Fixed Asset,هدف دارایی ثابت است, +Target Item Code,کد آیتم هدف, +Target Item Name,نام آیتم هدف, +Target Item {0} is neither a Fixed Asset nor a Stock Item,مورد هدف {0} نه یک دارایی ثابت است و نه یک کالای موجودی, +Target Item {0} must be a Fixed Asset item,مورد هدف {0} باید یک مورد دارایی ثابت باشد, +Target Item {0} must be a Stock Item,مورد هدف {0} باید یک مورد موجودی باشد, +Target Qty must be a positive number,تعداد هدف باید یک عدد مثبت باشد, +Target Serial No,شماره سریال هدف, +Target Warehouse is mandatory for Decapitalization,انبار هدف برای کاهش سرمایه اجباری است, +Target Warehouse is set for some items but the customer is not an internal customer.,انبار هدف برای برخی آیتم‌ها تنظیم شده است اما مشتری، یک مشتری داخلی نیست., +Task {0} depends on Task {1}. Please add Task {1} to the Tasks list.,وظیفه {0} به وظیفه {1} بستگی دارد. لطفاً وظیفه {1} را به لیست وظایف اضافه کنید., +Tax Amount,مقدار مالیات, +Tax Amount will be rounded on a row(items) level,مقدار مالیات در سطح ردیف (آیتم‌ها) گرد می شود, +Tax Refunds provided to Tourists under the Tax Refunds for Tourists Scheme,بازپرداخت مالیات بر اساس طرح بازپرداخت مالیات برای گردشگران به گردشگران ارائه می شود, +Tax Settings,تنظیمات مالیاتی, +Tax Withheld Vouchers,کوپن های مالیاتی کسر شده, +Tax Withholding Category {} against Company {} for Customer {} should have Cumulative Threshold value.,رده مالیاتی {} در برابر شرکت {} برای مشتری {} باید مقدار آستانه تجمعی داشته باشد., +Tax Withholding Details,جزئیات کسر مالیات, +Tax Withholding Net Total,کل خالص کسر مالیات, +Tax will be withheld only for amount exceeding the cumulative threshold,مالیات فقط برای مبلغی که بیش از آستانه تجمعی باشد، کسر خواهد شد, +Team,تیم, +Telephony Call Type,نوع تماس تلفنی, +Template Item Selected,آیتم الگو انتخاب شد, +Template Options,گزینه های الگو, +Template Task,وظیفه الگو, +Template Warnings,هشدارهای الگو, +Terms & Conditions,شرایط و ضوابط, +Terms Template,الگوی شرایط, +Territory Item,قلمرو مورد, +Territory Wise Sales,فروش از نظر منطقه, +The Condition '{0}' is invalid,"شرط ""{0}"" نامعتبر است", +The Document Type {0} must have a Status field to configure Service Level Agreement,نوع سند {0} باید دارای یک فیلد وضعیت برای پیکربندی قرارداد سطح سرویس باشد, +"The GL Entries will be cancelled in the background, it can take a few minutes.",ورودی‌های دفتر کل در پس‌زمینه لغو می‌شوند، ممکن است چند دقیقه طول بکشد., +"The Payment Request {0} is already paid, cannot process payment twice",درخواست پرداخت {0} قبلاً پرداخت شده است، نمی‌توان پرداخت را دو بار پردازش کرد, +"The Pick List having Stock Reservation Entries cannot be updated. If you need to make changes, we recommend canceling the existing Stock Reservation Entries before updating the Pick List.",فهرست انتخابی دارای ورودی های رزرو موجودی نمی تواند به روز شود. اگر نیاز به ایجاد تغییرات دارید، توصیه می‌کنیم قبل از به‌روزرسانی فهرست انتخاب، ورودی‌های رزرو موجودی را لغو کنید., +The Process Loss Qty has reset as per job cards Process Loss Qty,Process Loss Qty مطابق با کارت کارهای Process Loss Ty بازنشانی شده است, +The currency of invoice {} ({}) is different from the currency of this dunning ({}).,واحد پول فاکتور {} ({}) با واحد پول این دونینگ ({}) متفاوت است., +The default BOM for that item will be fetched by the system. You can also change the BOM.,BOM پیش‌فرض برای آن مورد توسط سیستم واکشی می‌شود. شما همچنین می توانید BOM را تغییر دهید., +The field {0} in row {1} is not set,فیلد {0} در ردیف {1} تنظیم نشده است, +"The following Items, having Putaway Rules, could not be accomodated:",موارد زیر که دارای قوانین Putaway هستند، قابل استفاده نیستند:, +The following assets have failed to automatically post depreciation entries: {0},دارایی های زیر به طور خودکار ورودی های استهلاک را پست نکرده اند: {0}, +The items {0} and {1} are present in the following {2} :,موارد {0} و {1} در {2} زیر موجود هستند:, +The operation {0} can not add multiple times,عملیات {0} نمی تواند چندین بار اضافه کند, +The operation {0} can not be the sub operation,عملیات {0} نمی تواند عملیات فرعی باشد, +The reserved stock will be released when you update items. Are you certain you wish to proceed?,با به‌روزرسانی موارد، موجودی رزرو شده آزاد می‌شود. آیا مطمئن هستید که می خواهید ادامه دهید؟, +The reserved stock will be released. Are you certain you wish to proceed?,موجودی رزرو شده آزاد خواهد شد. آیا مطمئن هستید که می خواهید ادامه دهید؟, +The selected {0} does not contain the selected Asset Item.,{0} انتخاب شده حاوی مورد دارایی انتخابی نیست., +"The stock has been reserved for the following Items and Warehouses, un-reserve the same to {0} the Stock Reconciliation:

{1}",موجودی برای اقلام و انبارهای زیر رزرو شده است، همان را در {0} تطبیق موجودی لغو کنید:

{1}, +"The sync has started in the background, please check the {0} list for new records.",همگام سازی در پس زمینه شروع شده است، لطفاً لیست {0} را برای رکوردهای جدید بررسی کنید., +"The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Submitted stage",وظیفه به عنوان یک کار پس زمینه در نوبت قرار گرفته است. در صورت وجود هرگونه مشکل در پردازش در پس‌زمینه، سیستم نظری در مورد خطا در این تطبیق موجودی اضافه می‌کند و به مرحله ارسال باز می‌گردد., +The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than allowed requested quantity {2} for Item {3},مجموع مقدار حواله / انتقال {0} در درخواست مواد {1} نمی تواند بیشتر از مقدار مجاز درخواستی {2} برای آیتم {3} باشد, +The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than requested quantity {2} for Item {3},مجموع مقدار حواله / انتقال {0} در درخواست مواد {1} نمی تواند بیشتر از مقدار درخواستی {2} برای آیتم {3} باشد, +"The users with this Role are allowed to create/modify a stock transaction, even though the transaction is frozen.",کاربران دارای این نقش مجاز به ایجاد/تغییر تراکنش موجودی هستند، حتی اگر تراکنش مسدود شده باشد., +The warehouse where you store finished Items before they are shipped.,انباری که اقلام تمام شده را قبل از ارسال در آن ذخیره می کنید., +"The warehouse where you store your raw materials. Each required item can have a separate source warehouse. Group warehouse also can be selected as source warehouse. On submission of the Work Order, the raw materials will be reserved in these warehouses for production usage.",انباری که مواد اولیه خود را در آن نگهداری می کنید. هر کالای مورد نیاز می تواند یک انبار منبع جداگانه داشته باشد. انبار گروهی نیز می تواند به عنوان انبار منبع انتخاب شود. پس از ارسال دستور کار، مواد اولیه در این انبارها برای استفاده تولید رزرو می شود., +The warehouse where your Items will be transferred when you begin production. Group Warehouse can also be selected as a Work in Progress warehouse.,انباری که هنگام شروع تولید، اقلام شما در آن منتقل می شوند. انبار گروهی همچنین می تواند به عنوان انبار Work in Progress انتخاب شود., +The {0} {1} is used to calculate the valuation cost for the finished good {2}.,{0} {1} برای محاسبه هزینه ارزیابی کالای نهایی {2} استفاده می‌شود., +There are no Failed transactions,هیچ تراکنش ناموفقی وجود ندارد, +There are no slots available on this date,هیچ اسلاتی در این تاریخ موجود نیست, +There are only {0} asset created or linked to {1}. Please create or link {2} Assets with respective document.,فقط {0} دارایی ایجاد شده یا به {1} پیوند داده شده است. لطفاً {2} دارایی ها را با سند مربوطه ایجاد یا پیوند دهید., +"There are two options to maintain valuation of stock. FIFO (first in - first out) and Moving Average. To understand this topic in detail please visit Item Valuation, FIFO and Moving Average.",دو گزینه برای حفظ ارزش موجودی وجود دارد. FIFO (اولین ورود - اولین خروج) و میانگین متحرک. برای درک جزئیات این موضوع، لطفاً از هدف ارزش اقلام، FIFO و میانگین متحرک., +There aren't any item variants for the selected item,هیچ گونه آیتمی برای آیتم انتخابی وجود ندارد, +There is already a valid Lower Deduction Certificate {0} for Supplier {1} against category {2} for this time period.,در حال حاضر یک گواهی کسر کمتر معتبر {0} برای تامین کننده {1} در برابر دسته {2} برای این دوره زمانی وجود دارد., +There is already an active Subcontracting BOM {0} for the Finished Good {1}.,در حال حاضر یک BOM قرارداد فرعی فعال {0} برای کالای نهایی {1} وجود دارد., +There must be atleast 1 Finished Good in this Stock Entry,باید حداقل 1 کالای تمام شده در این ثبت موجودی وجود داشته باشد, +There was an error creating Bank Account while linking with Plaid.,هنگام پیوند با Plaid خطایی در ایجاد حساب بانکی روی داد., +There was an error syncing transactions.,هنگام همگام‌سازی تراکنش‌ها خطایی روی داد., +There was an error updating Bank Account {} while linking with Plaid.,هنگام به‌روزرسانی حساب بانکی {} هنگام پیوند با Plaid خطایی روی داد., +There was an issue connecting to Plaid's authentication server. Check browser console for more information,مشکلی در اتصال به سرور تأیید اعتبار Plaid وجود داشت. برای اطلاعات بیشتر کنسول مرورگر را بررسی کنید, +There were issues unlinking payment entry {0}.,مشکلاتی در قطع پیوند ثبت پرداخت {0} وجود داشت., +This Account has '0' balance in either Base Currency or Account Currency,این حساب دارای موجودی '0' به ارز پایه یا ارز حساب است, +This field is used to set the 'Customer'.,"این فیلد برای تنظیم ""مشتری"" استفاده می شود.", +This filter will be applied to Journal Entry.,این فیلتر برای ثبت دفتر روزنامه اعمال خواهد شد., +This is a Template BOM and will be used to make the work order for {0} of the item {1},این یک الگوی BOM است و برای ایجاد دستور کار برای {0} مورد {1} استفاده خواهد شد., +This is considered dangerous from accounting point of view.,این از نظر حسابداری خطرناک تلقی می شود., +"This is enabled by default. If you want to plan materials for sub-assemblies of the Item you're manufacturing leave this enabled. If you plan and manufacture the sub-assemblies separately, you can disable this checkbox.",این به طور پیش فرض فعال است. اگر می‌خواهید مواد را برای زیر مونتاژ های آیتمی که در حال تولید آن هستید برنامه‌ریزی کنید، این گزینه را فعال کنید. اگر زیر مونتاژ ها را جداگانه برنامه ریزی و تولید می کنید، می توانید این چک باکس را غیرفعال کنید., +"This is for raw material Items that'll be used to create finished goods. If the Item is an additional service like 'washing' that'll be used in the BOM, keep this unchecked.","این برای اقلام مواد خام است که برای ایجاد کالاهای نهایی استفاده می شود. اگر مورد یک سرویس اضافی مانند ""شستن"" است که در BOM استفاده می شود، این مورد را علامت نزنید.", +This item filter has already been applied for the {0},این فیلتر مورد قبلاً برای {0} اعمال شده است, +This option can be checked to edit the 'Posting Date' and 'Posting Time' fields.,این گزینه برای ویرایش فیلدهای «تاریخ ارسال» و «زمان ارسال» قابل بررسی است., +This schedule was created when Asset {0} was adjusted through Asset Value Adjustment {1}.,این برنامه زمانی ایجاد شد که دارایی {0} از طریق تعدیل ارزش دارایی {1} تنظیم شد., +This schedule was created when Asset {0} was consumed through Asset Capitalization {1}.,این برنامه زمانی ایجاد شد که دارایی {0} از طریق سرمایه گذاری دارایی {1} مصرف شد., +This schedule was created when Asset {0} was repaired through Asset Repair {1}.,این برنامه زمانی ایجاد شد که دارایی {0} از طریق تعمیر دارایی {1} تعمیر شد., +This schedule was created when Asset {0} was restored on Asset Capitalization {1}'s cancellation.,این برنامه زمانی ایجاد شد که دارایی {0} در لغو دارایی با حروف بزرگ {1} بازیابی شد., +This schedule was created when Asset {0} was restored.,این برنامه زمانی ایجاد شد که دارایی {0} بازیابی شد., +This schedule was created when Asset {0} was returned through Sales Invoice {1}.,این برنامه زمانی ایجاد شد که دارایی {0} از طریق فاکتور فروش {1} برگردانده شد., +This schedule was created when Asset {0} was scrapped.,این برنامه زمانی ایجاد شد که دارایی {0} لغو شد., +This schedule was created when Asset {0} was sold through Sales Invoice {1}.,این برنامه زمانی ایجاد شد که دارایی {0} از طریق فاکتور فروش {1} فروخته شد., +This schedule was created when Asset {0} was updated after being split into new Asset {1}.,این برنامه زمانی ایجاد شد که دارایی {0} پس از تقسیم به دارایی جدید {1} به روز شد., +This schedule was created when Asset {0}'s Asset Repair {1} was cancelled.,این برنامه زمانی ایجاد شد که تعمیر دارایی {0} {1} لغو شد., +This schedule was created when Asset {0}'s Asset Value Adjustment {1} was cancelled.,این برنامه زمانی ایجاد شد که تعدیل ارزش دارایی {0} {1} لغو شد., +This schedule was created when Asset {0}'s shifts were adjusted through Asset Shift Allocation {1}.,این برنامه زمانی ایجاد شد که تغییرات دارایی {0} از طریق تخصیص تغییر دارایی {1} تنظیم شد., +This schedule was created when new Asset {0} was split from Asset {1}.,این برنامه زمانی ایجاد شد که دارایی جدید {0} از دارایی {1} جدا شد., +"This table is used to set details about the 'Item', 'Qty', 'Basic Rate', etc.","این جدول برای تنظیم جزئیات مربوط به ""آیتم""، ""مقدار""، ""نرخ پایه"" و غیره استفاده می شود.", +This {} will be treated as material transfer.,این {} به عنوان انتقال مواد در نظر گرفته می شود., +Threshold for Suggestion (In Percentage),آستانه پیشنهاد (در درصد), +Time Taken to Deliver,زمان صرف شده برای تحویل, +Time in mins,زمان به دقیقه, +Time in mins.,زمان به دقیقه., +Time slot is not available,بازه زمانی در دسترس نیست, +To Delivery Date,تا تاریخ تحویل, +To Doctype,برای Doctype, +To Due Date,به تاریخ سررسید, +To Payment Date,به تاریخ پرداخت, +To Reference Date,به تاریخ مرجع, +To add Operations tick the 'With Operations' checkbox.,"برای افزودن عملیات، کادر ""با عملیات"" را علامت بزنید.", +To add subcontracted Item's raw materials if include exploded items is disabled.,افزودن مواد خام قراردادی فرعی در صورت وجود آیتم‌های گسترده شده غیرفعال است., +To apply condition on parent field use parent.field_name and to apply condition on child table use doc.field_name. Here field_name could be based on the actual column name of the respective field.,برای اعمال شرط در فیلد والد از parent.field_name و برای اعمال شرط در جدول فرزند از doc.field_name استفاده کنید. در اینجا field_name می تواند بر اساس نام ستون واقعی فیلد مربوطه باشد., +To be Delivered to Customer,برای تحویل به مشتری, +To cancel a {} you need to cancel the POS Closing Entry {}.,برای لغو یک {}، باید ثبت اختتامیه POS {} را لغو کنید., +"To enable Capital Work in Progress Accounting,",برای فعال کردن حسابداری کار سرمایه ای،, +To include non-stock items in the material request planning. i.e. Items for which 'Maintain Stock' checkbox is unticked.,"گنجاندن آیتم‌های غیر موجودی در برنامه ریزی درخواست مواد. به عنوان مثال آیتم‌هایی که چک باکس ""نگهداری موجودی"" برای آنها علامت گذاری نشده است.", +To submit the invoice without purchase order please set {0} as {1} in {2},برای ارسال فاکتور بدون سفارش خرید لطفاً {0} را به عنوان {1} در {2} تنظیم کنید, +To submit the invoice without purchase receipt please set {0} as {1} in {2},برای ارسال فاکتور بدون رسید خرید، لطفاً {0} را به عنوان {1} در {2} تنظیم کنید., +"To use a different finance book, please uncheck 'Include Default FB Assets'",برای استفاده از یک کتاب مالی متفاوت، لطفاً علامت «شامل دارایی‌های پیش‌فرض FB» را بردارید., +"To use a different finance book, please uncheck 'Include Default FB Entries'",برای استفاده از یک کتاب مالی متفاوت، لطفاً علامت «شامل ورودی‌های پیش‌فرض FB» را بردارید., +Total Active Items,مجموع آیتم‌های فعال, +Total Allocations,مجموع تخصیص ها, +Total Asset,کل دارایی, +Total Asset Cost,هزینه کل دارایی, +Total Billing Hours,کل ساعت صورتحساب, +Total Contribution Amount Against Invoices: {0},مجموع مبلغ مشارکت در برابر فاکتورها: {0}, +Total Contribution Amount Against Orders: {0},کل مبلغ مشارکت در برابر سفارش‌ها: {0}, +Total Equity,مجموع حقوق صاحبان موجودی, +Total Incoming Bills,مجموع صورتحساب های دریافتی, +Total Incoming Payment,کل پرداخت ورودی, +Total Incoming Value (Receipt),مجموع ارزش ورودی (رسید), +Total Interest,سود کل, +Total Issues,مجموع مشکلات, +Total Items,مجموع آیتم‌ها, +Total Liability,کل مسئولیت, +Total Operation Time,کل زمان عملیات, +Total Other Charges,مجموع سایر هزینه ها, +Total Outgoing Bills,مجموع صورتحساب خروجی, +Total Outgoing Payment,کل پرداخت خروجی, +Total Outgoing Value (Consumption),کل ارزش خروجی (مصرف), +Total Purchase Amount,کل مبلغ خرید, +Total Purchase Cost has been updated,کل هزینه خرید به روز شده است, +Total Repair Cost,کل هزینه تعمیر, +Total Reposting Count,تعداد کل بازنشر, +Total Sales Amount,کل مبلغ فروش, +Total Stock Value,ارزش کل موجودی, +Total Supplied Qty,تعداد کل عرضه شده, +Total Time (in Mins),زمان کل (به دقیقه), +Total Value,ارزش کل, +Total Value Difference (Incoming - Outgoing),تفاوت ارزش کل (ورودی - خروجی), +Total Views,کل بازدیدها, +Total Warehouses,کل انبارها, +Total percentage against cost centers should be 100,درصد کل در مقابل مراکز هزینه باید 100 باشد, +Tracking Status,وضعیت پیگیری, +Tracking Status Info,اطلاعات وضعیت ردیابی, +Tracking URL,URL پیگیری, +Transaction Deletion Document: {0} is running for this Company. {1},سند حذف تراکنش: {0} برای این شرکت در حال اجرا است. {1}, +Transaction Deletion Record,رکورد حذف تراکنش, +Transaction Deletion Record Item,مورد رکورد حذف تراکنش, +Transaction Exchange Rate,نرخ ارز معاملات, +Transaction Settings,تنظیمات تراکنش, +Transactions against the Company already exist! Chart of Accounts can only be imported for a Company with no transactions.,معاملات در مقابل شرکت در حال حاضر وجود دارد! نمودار حساب ها فقط برای شرکتی بدون تراکنش قابل درون‌بُرد است., +Transfer Asset,انتقال دارایی, +Transfer From Warehouses,انتقال از انبارها, +Transferring cannot be done to an Employee. Please enter location where Asset {0} has to be transferred,انتقال به کارمند امکان پذیر نیست. لطفاً مکانی را وارد کنید که دارایی {0} باید در آنجا منتقل شود, +Transit,ترانزیت, +Transit Entry,ورودی حمل و نقل, +Truncates 'Remarks' column to set character length,"ستون ""Remarks"" را برای تنظیم طول کاراکتر کوتاه می کند", +Type Of Call,نوع تماس, +Type of Transaction,نوع تراکنش, +UAE VAT 201,مالیات بر ارزش افزوده امارات متحده عربی 201, +UAE VAT Account,حساب مالیات بر ارزش افزوده امارات متحده عربی, +UAE VAT Accounts,حساب های مالیات بر ارزش افزوده امارات متحده عربی, +UAE VAT Settings,تنظیمات مالیات بر ارزش افزوده امارات متحده عربی, +UOM conversion factor required for UOM: {0} in Item: {1},ضریب تبدیل UOM مورد نیاز برای UOM: {0} در مورد: {1}, +UnReconcile,تطبیق نکردن, +Unable to find variable:,قادر به یافتن متغیر نیست:, +Unassigned Qty,تعداد تعیین نشده, +"Under Working Hours table, you can add start and end times for a Workstation. For example, a Workstation may be active from 9 am to 1 pm, then 2 pm to 5 pm. You can also specify the working hours based on shifts. While scheduling a Work Order, the system will check for the availability of the Workstation based on the working hours specified.",در جدول ساعات کاری، می توانید زمان شروع و پایان یک ایستگاه کاری را اضافه کنید. به عنوان مثال، یک ایستگاه کاری ممکن است از ساعت 9 صبح تا 1 بعد از ظهر و سپس از 2 بعد از ظهر تا 5 بعد از ظهر فعال باشد. همچنین می توانید ساعت کاری را بر اساس شیفت ها مشخص کنید. هنگام برنامه ریزی یک دستور کار، سیستم بر اساس ساعات کاری مشخص شده، در دسترس بودن ایستگاه کاری را بررسی می کند., +Unit of Measure (UOM),واحد اندازه گیری (UOM), +Unlinked,بدون پیوند, +Unqualified,فاقد صلاحیت, +Unrealized Profit / Loss Account,حساب سود / زیان تحقق نیافته, +Unrealized Profit / Loss account for intra-company transfers,حساب سود / زیان تحقق نیافته برای نقل و انتقالات درون شرکتی, +Unrealized Profit/Loss account for intra-company transfers,حساب سود/زیان تحقق نیافته برای نقل و انتقالات درون شرکتی, +Unreconcile Payment,پرداخت ناسازگار, +Unreconcile Payment Entries,ورودی های پرداخت ناسازگار, +Unreconcile Transaction,تراکنش ناسازگار, +Unreconciled Amount,مبلغ ناسازگار, +Unreconciled Entries,ورودی های تطبیق نگرفته, +Unreserve,لغو رزرو کنید, +Unreserve Stock,ذخیره موجودی, +Unreserving Stock...,عدم رزرو موجودی..., +Up,بالا, +Update Billed Amount in Delivery Note,مبلغ صورتحساب در یادداشت تحویل را به روز کنید, +Update Existing Price List Rate,به روز رسانی نرخ لیست قیمت موجود, +Update Existing Records,به روز رسانی رکوردهای موجود, +Update Rate as per Last Purchase,نرخ به روز رسانی بر اساس آخرین خرید, +Update Total Purchase Cost,به روز رسانی کل هزینه خرید, +Update frequency of Project,فرکانس به روز رسانی پروژه, +Update stock must be enabled for the purchase invoice {0},به‌روزرسانی موجودی باید برای فاکتور خرید فعال شود {0}, +Updated via 'Time Log' (In Minutes),"به روز شده از طریق ""Time Log"" (به دقیقه)", +Updating Work Order status,به روز رسانی وضعیت دستور کار, +"Updating {0} of {1}, {2}",در حال به روز رسانی {0} از {1}، {2}, +Upload Bank Statement,بارگذاری صورتحساب بانکی, +Use 'Repost in background' button to trigger background job. Job can only be triggered when document is in Queued or Failed status.,برای فعال کردن کار پس‌زمینه از دکمه «بازنشر در پس‌زمینه» استفاده کنید. کار فقط زمانی می تواند فعال شود که سند در وضعیت صف یا ناموفق باشد., +Use Batch-wise Valuation,استفاده از ارزش گذاری دسته ای, +Use Company Default Round Off Cost Center,از مرکز هزینه دور پیش فرض شرکت استفاده کنید, +Use Company default Cost Center for Round off,از مرکز هزینه پیش‌فرض شرکت برای دور کردن استفاده کنید, +Use HTTP Protocol,استفاده از پروتکل HTTP, +Use Item based reposting,از ارسال مجدد بر اساس آیتم استفاده کنید, +Use Serial No / Batch Fields,از فیلدهای شماره سریال / دسته استفاده کنید, +Use Transaction Date Exchange Rate,از نرخ مبادله تاریخ تراکنش استفاده کنید, +User {0}: Removed Employee Self Service role as there is no mapped employee.,کاربر {0}: نقش خود سرویس کارمند حذف شد زیرا کارمند نگاشت شده وجود ندارد., +User {0}: Removed Employee role as there is no mapped employee.,کاربر {0}: نقش کارمند حذف شد زیرا کارمند نگاشت شده وجود ندارد., +Users can enable the checkbox If they want to adjust the incoming rate (set using purchase receipt) based on the purchase invoice rate.,اگر کاربران بخواهند نرخ ورودی (تنظیم با استفاده از رسید خرید) را بر اساس نرخ فاکتور خرید تنظیم کنند، می توانند کادر انتخاب را فعال کنند., +Users with this role are allowed to over bill above the allowance percentage,کاربرانی که این نقش را دارند مجاز به اضافه صورتحساب بالاتر از درصد مجاز هستند, +Users with this role are allowed to over deliver/receive against orders above the allowance percentage,کاربرانی که این نقش را دارند مجاز به بیش تحویل/دریافت سفارش‌ها بالاتر از درصد مجاز هستند, +Using negative stock disables FIFO/Moving average valuation when inventory is negative.,استفاده از موجودی منفی، ارزش گذاری FIFO / میانگین متحرک را زمانی که موجودی کالا منفی است، غیرفعال می کند., +VAT Accounts,حساب های مالیات بر ارزش افزوده, +VAT Amount (AED),مقدار VAT (AED), +VAT Audit Report,گزارش حسابرسی مالیات بر ارزش افزوده, +VAT on Expenses and All Other Inputs,مالیات بر ارزش افزوده هزینه ها و سایر ورودی ها, +VAT on Sales and All Other Outputs,مالیات بر ارزش افزوده بر فروش و سایر خروجی ها, +Valid From must be after {0} as last GL Entry against the cost center {1} posted on this date,معتبر از باید بعد از {0} به عنوان آخرین ثبت دفتر کل در برابر مرکز هزینه {1} پست شده در این تاریخ باشد, +Validate Negative Stock,اعتبار موجودی منفی, +Validate Stock on Save,اعتبار موجودی در ذخیره, +Valuation Field Type,نوع فیلد ارزش گذاری, +Valuation Rate (In / Out),نرخ ارزش گذاری (ورودی/خروجی), +Valuation rate for customer provided items has been set to zero.,نرخ ارزش گذاری برای آیتم‌های ارائه شده توسط مشتری صفر تعیین شده است., +Value Based Inspection,بازرسی مبتنی بر ارزش, +Value Change,تغییر ارزش, +Value Details,جزئیات ارزش, +Value of Goods,ارزش کالاها, +Value of goods cannot be 0,ارزش کالا نمی تواند 0 باشد, +Verification failed please check the link,تأیید انجام نشد لطفاً پیوند را بررسی کنید, +Via Landed Cost Voucher,از طریق کوپن هزینه تمام شده تا درب انبار, +View BOM Update Log,مشاهده لاگ به‌روزرسانی BOM, +View Exchange Gain/Loss Journals,مشاهده دفترهای روزنامه سود/زیان تبادل, +View General Ledger,مشاهده دفتر کل مرکزی, +View Ledgers,مشاهده دفتر کل ها, +Visits,بازدیدها, +Voice Call Settings,تنظیمات تماس صوتی, +Voucher,کوپن, +Voucher Name,نام کوپن, +Voucher No is mandatory,شماره کوپن الزامی است, +Voucher Qty,مقدار کوپن, +Voucher Subtype,زیرنوع کوپن, +Voucher {0} is over-allocated by {1},کوپن {0} توسط {1} بیش از حد تخصیص داده شده است, +Voucher {0} value is broken: {1},ارزش کوپن {0} خراب است: {1}, +Voucher-wise Balance,تراز مبتنی بر کوپن, +"WARNING: Exotel app has been separated from ERPNext, please install the app to continue using Exotel integration.",هشدار: برنامه Exotel از ERPNext جدا شده است، لطفاً برای ادامه استفاده از Exotel یکپارچه سازی، برنامه را نصب کنید., +WIP Composite Asset,دارایی ترکیبی «کار در حال انجام», +Waiting for payment...,در انتظار پرداخت..., +Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} {2}.,"ظرفیت انبار برای آیتم ""{0}"" باید بیشتر از سطح موجودی موجود در {1} {2} باشد.", +Warehouse Details,جزئیات انبار, +Warehouse Disabled?,انبار غیر فعال است؟, +Warehouse Settings,تنظیمات انبار, +Warehouse Wise Stock Balance,تراز موجودی مبتنی بر انبار, +Warehouse wise Stock Value,ارزش موجودی مبتنی بر انبار, +Warehouse {0} does not belong to Company {1}.,انبار {0} متعلق به شرکت {1} نیست., +"Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}.",انبار {0} به هیچ حسابی مرتبط نیست، لطفاً حساب را در سابقه انبار ذکر کنید یا حساب موجودی پیش فرض را در شرکت {1} تنظیم کنید., +Warehouse's Stock Value has already been booked in the following accounts:,ارزش موجودی انبار قبلاً در حساب های زیر رزرو شده است:, +Warning - Row {0}: Billing Hours are more than Actual Hours,هشدار - ردیف {0}: ساعات صورتحساب بیشتر از ساعت‌های واقعی است, +Warning on Negative Stock,هشدار در مورد موجودی منفی, +Warning!,هشدار!, +Watch Video,تماشای ویدیو, +Website Script,اسکریپت وب سایت, +Website Theme,تم وب سایت, +Week {0} {1},هفته {0} {1}, +Weekly Time to send,زمان ارسال هفتگی, +Weight (kg),وزن (کیلوگرم), +"When a parent warehouse is chosen, the system conducts stock checks against the associated child warehouses",هنگامی که یک انبار اصلی انتخاب می شود، سیستم بررسی های موجودی را در مقابل انبارهای فرزند مرتبط انجام می دهد, +"When creating an Item, entering a value for this field will automatically create an Item Price at the backend.",هنگام ایجاد یک آیتم، با وارد کردن یک مقدار برای این فیلد، به طور خودکار قیمت آیتم در قسمت پشتیبان ایجاد می شود., +"While making Purchase Invoice from Purchase Order, use Exchange Rate on Invoice's transaction date rather than inheriting it from Purchase Order. Only applies for Purchase Invoice.",هنگام تهیه فاکتور خرید از سفارش خرید، به جای ارث بردن آن از سفارش خرید، از نرخ مبادله در تاریخ تراکنش فاکتور استفاده کنید. فقط برای فاکتور خرید اعمال می شود., +Width (cm),عرض (سانتی متر), +Withdrawal,برداشت از حساب, +Work Order / Subcontract PO,دستور کار / PO قرارداد فرعی, +Work Order Consumed Materials,مواد مصرفی دستور کار, +Workflow,جریان کار, +Workflow Action,عمل گردش کار, +Workflow State,وضعیت گردش کار, +Workstation Dashboard,داشبورد ایستگاه کاری, +Workstation Status,وضعیت ایستگاه کاری, +Workstation Type,نوع ایستگاه کاری, +Workstations,ایستگاه های کاری, +Write Off Limit,محدودیت نوشتن, +Wrong Company,شرکت اشتباه, +Wrong Template,الگوی اشتباه, +You are not authorized to make/edit Stock Transactions for Item {0} under warehouse {1} before this time.,شما مجاز به انجام/ویرایش معاملات موجودی برای کالای {0} در انبار {1} قبل از این زمان نیستید., +You are picking more than required quantity for the item {0}. Check if there is any other pick list created for the sales order {1}.,شما در حال انتخاب بیش از مقدار مورد نیاز برای مورد {0} هستید. بررسی کنید که آیا لیست انتخاب دیگری برای سفارش فروش {1} ایجاد شده است., +"You can set it as a machine name or operation type. For example, stiching machine 12",می توانید آن را به عنوان نام ماشین یا نوع عملیات تنظیم کنید. مثلا ماشین دوخت 12, +You can't make any changes to Job Card since Work Order is closed.,از آنجایی که دستور کار بسته شده است، نمی توانید هیچ تغییری در کارت کار ایجاد کنید., +You can't redeem Loyalty Points having more value than the Rounded Total.,نمی‌توانید امتیازهای وفاداری را که ارزش بیشتری از مجموع گرد شده دارند، پس‌خرید کنید., +You cannot change the rate if BOM is mentioned against any Item.,اگر BOM در برابر هر موردی ذکر شده باشد، نمی توانید نرخ را تغییر دهید., +You cannot create a {0} within the closed Accounting Period {1},شما نمی توانید یک {0} در دوره حسابداری بسته {1} ایجاد کنید, +You cannot create/amend any accounting entries till this date.,تا این تاریخ نمی توانید هیچ ورودی حسابداری ایجاد یا اصلاح کنید., +You cannot repost item valuation before {},شما نمی توانید ارزیابی مورد را قبل از {} دوباره ارسال کنید, +You have entered a duplicate Delivery Note on Row,شما یک یادداشت تحویل تکراری در ردیف وارد کرده اید, +You haven't created a {0} yet,شما هنوز یک {0} ایجاد نکرده اید, +You need to cancel POS Closing Entry {} to be able to cancel this document.,برای اینکه بتوانید این سند را لغو کنید، باید ثبت اختتامیه POS {} را لغو کنید., +Your Name (required),نام شما (الزامی), +Your email has been verified and your appointment has been scheduled,ایمیل شما تایید شده و قرار ملاقات شما تعیین شده است, +Zero Balance,تراز صفر, +Zero Rated,دارای امتیاز صفر, +Zero quantity,مقدار صفر, +`Allow Negative rates for Items`,«نرخ های منفی برای آیتم‌ها مجاز است», +as a percentage of finished item quantity,به عنوان درصدی از مقدار کالای تمام شده, +at,در, +description,شرح, +discount applied,تخفیف اعمال شد, +doc_type,نوع_doc, +exchangerate.host,مبادله.میزبان, +is already,است در حال حاضر, +must be between 0 and 100,باید بین 0 تا 100 باشد, +or its descendants,یا فرزندان آن, +out of 5,از 5, +payments app is not installed. Please install it from {0} or {1},برنامه پرداخت نصب نشده است لطفاً آن را از {0} یا {1} نصب کنید, +payments app is not installed. Please install it from {} or {},برنامه پرداخت نصب نشده است لطفاً آن را از {} یا {} نصب کنید, +performing either one below:,انجام هر یک از موارد زیر:, +product bundle item row's name in sales order. Also indicates that picked item is to be used for a product bundle,نام ردیف آیتم‌های باندل محصول در سفارش فروش. همچنین نشان می دهد که آیتم انتخاب شده قرار است برای یک باندل محصول استفاده شود, +ratings,رتبه بندی ها, +subscription is already cancelled.,اشتراک در حال حاضر لغو شده است., +temporary name,نام موقت, +to unallocate the amount of this Return Invoice before cancelling it.,برای تخصیص مبلغ این فاکتور برگشتی قبل از لغو آن., +variance,واریانس, +via BOM Update Tool,از طریق BOM Update Tool, +will be,خواهد بود, +{0} {1} has submitted Assets. Remove Item {2} from table to continue.,{0} {1} دارایی‌ها را ارسال کرده است. برای ادامه، آیتم {2} را از جدول حذف کنید., +{0} Account not found against Customer {1}.,{0} حساب در مقابل مشتری پیدا نشد {1}., +{0} Budget for Account {1} against {2} {3} is {4}. It {5} exceed by {6},{0} بودجه برای حساب {1} در برابر {2} {3} {4} است. {5} از {6} بیشتر است, +{0} Transaction(s) Reconciled,{0} تراکنش(های) تطبیق شد, +{0} account is not of type {1},حساب {0} از نوع {1} نیست, +{0} account not found while submitting purchase receipt,هنگام ارسال رسید خرید، حساب {0} پیدا نشد, +{0} and {1},{0} و {1}, +{0} cannot be used as a Main Cost Center because it has been used as child in Cost Center Allocation {1},{0} نمی‌تواند به‌عنوان مرکز هزینه اصلی استفاده شود زیرا به‌عنوان فرزند در تخصیص مرکز هزینه {1} استفاده شده است., +{0} cannot be zero,{0} نمی تواند صفر باشد, +{0} currency must be same as company's default currency. Please select another account.,ارز {0} باید با واحد پول پیش‌فرض شرکت یکسان باشد. لطفا حساب دیگری را انتخاب کنید., +{0} entered twice {1} in Item Taxes,{0} دو بار {1} در مالیات آیتم وارد شد, +{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section,{0} تخصیص مبتنی بر مدت پرداخت را فعال کرده است. در بخش مراجع پرداخت، یک شرایط پرداخت برای ردیف #{1} انتخاب کنید, +{0} is a mandatory Accounting Dimension.
Please set a value for {0} in Accounting Dimensions section.,{0} یک بعد حسابداری اجباری است.
لطفاً یک مقدار برای {0} در بخش ابعاد حسابداری تنظیم کنید., +{0} is added multiple times on rows: {1},{0} چندین بار در ردیف ها اضافه می شود: {1}, +{0} is already running for {1},{0} در حال حاضر برای {1} در حال اجرا است, +{0} is mandatory for account {1},{0} برای حساب {1} اجباری است, +{0} qty of Item {1} is being received into Warehouse {2} with capacity {3}.,{0} تعداد مورد {1} در انبار {2} با ظرفیت {3} در حال دریافت است., +"{0} units are reserved for Item {1} in Warehouse {2}, please un-reserve the same to {3} the Stock Reconciliation.",{0} واحد برای مورد {1} در انبار {2} رزرو شده است، لطفاً همان را در {3} تطبیق موجودی لغو کنید., +{0} units of Item {1} is picked in another Pick List.,{0} واحد از مورد {1} در فهرست انتخاب دیگری انتخاب شده است., +"{0} units of {1} are required in {2}{3}, on {4} {5} for {6} to complete the transaction.",{0} واحد از {1} در {2}{3}، در {4} {5} برای {6} برای تکمیل تراکنش مورد نیاز است., +{0} units of {1} needed in {2} on {3} {4} to complete this transaction.,برای تکمیل این تراکنش به {0} واحد از {1} در {2} در {3} {4} نیاز است., +{0} will be given as discount.,{0} به عنوان تخفیف داده می شود., +{0} {1} Manually,{0} {1} به صورت دستی, +{0} {1} Partially Reconciled,{0} {1} تا حدی تطبیق کرد, +"{0} {1} cannot be updated. If you need to make changes, we recommend canceling the existing entry and creating a new one.",{0} {1} نمی تواند به روز شود. اگر نیاز به ایجاد تغییرات دارید، توصیه می کنیم ورودی موجود را لغو کنید و یک ورودی جدید ایجاد کنید., +{0} {1} has already been fully paid.,{0} {1} قبلاً به طور کامل پرداخت شده است., +{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts.,{0} {1} قبلاً تا حدی پرداخت شده است. لطفاً از دکمه «دریافت صورتحساب معوق» یا «دریافت سفارش‌های معوق» برای دریافت آخرین مبالغ معوق استفاده کنید., +{0} {1} is allocated twice in this Bank Transaction,{0} {1} دو بار در این تراکنش بانکی تخصیص داده شده است, +{0} {1} is not in any active Fiscal Year,{0} {1} در هیچ سال مالی فعالی نیست, +{0} {1} is on hold,{0} {1} در انتظار است, +{0} {1} not allowed to be reposted. Modify {2} to enable reposting.,{0} {1} مجاز به ارسال مجدد نیست. برای فعال کردن ارسال مجدد، {2} را تغییر دهید., +{0} {1} via CSV File,{0} {1} از طریق فایل CSV, +{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions,{0} {1}: حساب {2} یک حساب گروهی است و نمی توان از حساب های گروهی در تراکنش ها استفاده کرد, +{0} {1}: Cost Center is required for 'Profit and Loss' account {2}.,"{0} {1}: مرکز هزینه برای حساب ""سود و زیان"" {2} لازم است.", +{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions,{0} {1}: مرکز هزینه {2} یک مرکز هزینه گروهی است و مراکز هزینه گروهی را نمی توان در تراکنش ها استفاده کرد, +{0}% of total invoice value will be given as discount.,{0}% از ارزش کل فاکتور به عنوان تخفیف داده می شود., +{0}'s {1} cannot be after {2}'s Expected End Date.,{1} {0} نمی تواند بعد از تاریخ پایان مورد انتظار {2} باشد., +{item_name}'s Sample Size ({sample_size}) cannot be greater than the Accepted Quantity ({accepted_quantity}),اندازه نمونه {item_name} ({sample_size}) نمی‌تواند بیشتر از مقدار مورد قبول ({accepted_quantity}) باشد., +{} Available,{} در دسترس, +{} To Deliver,{} برای تحویل, +{} To Receive,{} برای دریافت, +{} Assigned,{} اختصاص یافته, +{} Available,{} در دسترس, +{} Open,{} باز, +{} Pending,{} انتظار, +{} To Bill,{} برای صورتحساب, +{} is a child company.,{} یک شرکت فرزند است., +{} {} is already linked with another {},{} {} قبلاً با {} دیگری پیوند شده است, +{} {} is already linked with {} {},{} {} قبلاً با {} {} پیوند داده شده است, diff --git a/erpnext/translations/sv.csv b/erpnext/translations/sv.csv index 4a53c63fafae..ca0782fb728b 100644 --- a/erpnext/translations/sv.csv +++ b/erpnext/translations/sv.csv @@ -8743,3 +8743,3086 @@ WhatsApp,WhatsApp, Make a call,Ringa ett samtal, Approve,Godkänna, Reject,Avvisa, + Address, Adress, + Amount,Belopp, + Is Child Table,Är Undertabell, + Name,Namn, + Rate,Moms %, + Summary,Översikt, +"""SN-01::10"" for ""SN-01"" to ""SN-10""","""SN-01::10"" för ""SN-01"" till ""SN-10""", +# In Stock,# I Lager, +# Req'd Items,# Erfodrade Artiklar, +% Finished Item Quantity,% Färdig Artikel Kvantitet, +% Occupied,% Upptagen, +% Picked,% Plockad, +% Process Loss,% Process Förlust, +% Returned,% Retur, +'Account' in the Accounting section of Customer {0},"""Konto"" i Bokföring Sektion för Kund {0}", +'Allow Multiple Sales Orders Against a Customer's Purchase Order',"""Tillåt flera Försäljning Order mot Kund Inköp Order""", +'Default {0} Account' in Company {1},"""Standard {0} Konto"" i Bolag {1}", +'To Package No.' cannot be less than 'From Package No.',"""Till Paket Nummer."" får inte vara lägre än ""Från Paket Nummer.""", +'{0}' account is already used by {1}. Use another account.,'{0}' konto används redan av {1}. Använd ett annat konto., +'{0}' should be in company currency {1}.,"""{0}"" ska vara i bolag valuta {1}.", +(A) Qty After Transaction,(A) Kvantitet Efter Transaktion, +(B) Expected Qty After Transaction,(B) Förväntad Kvantitet Efter Transaktion, +(C) Total Qty in Queue,(C) Totalt Kvantitet i Kö, +(C) Total qty in queue,(C) Totalt Kvantitet i Kö, +(D) Balance Stock Value,(D) Saldo Lager Värde, +(E) Balance Stock Value in Queue,(E) Saldo Lager Värde i Kö, +(F) Change in Stock Value,(F) Förändring i Lager Värde, +(G) Sum of Change in Stock Value,(G) Summan av Förändring i Lager Värde, +(H) Change in Stock Value (FIFO Queue),(H) Förändring av Lager Värde (FIFO), +(H) Valuation Rate,(H) Grund Pris, +(I) Valuation Rate,(I) Grund Pris, +(J) Valuation Rate as per FIFO,(J) Grund Pris enligt FIFO, +(K) Valuation = Value (D) ÷ Qty (A),(K) Värdering = Värde (D) ÷ Kvantitet (A), +", with the inventory {0}: {1}",", med inventering {0}: {1}", +0-30 Days,0-30 Dagar, +3 Yearly,Var Tredje År, +30-60 Days,30-60 Dagar, +60-90 Days,60 - 90 Dagar, +90 Above,90+ Dagar, +"
+

Note

+
    +
  • +You can use Jinja tags in Subject and Body fields for dynamic values. +
  • + All fields in this doctype are available under the doc object and all fields for the customer to whom the mail will go to is available under the customer object. +
+

Examples

+ +
    +
  • Subject:

    Statement Of Accounts for {{ customer.customer_name }}

  • +
  • Body:

    +
    Hello {{ customer.customer_name }},
    PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}.
  • +
+","
+

Observera

+
    +
  • +Du kan använda Jinja Taggar i Ämne och Huvudtext-fält för dynamiska värden. +
  • + Alla fält i denna doctype är tillgängliga under doc-objekt och alla fält för kund som e-post meddelande går till är tillgängliga under customer-objekt. +
+

Exempel

+ +
    +
  • Ämne:

    Konto besked för {{ customer.customer_name }}

  • +
  • Huvudtext:

    +
    Hej {{ customer.customer_name }},
    Kontoutdrag från {{ doc.from_date }} till {{ doc.to_date }}.
    +
+", +"
Other Details
","
Övriga Detaljer
", +"
No Matching Bank Transactions Found
","
Inga avstämda Bank Transaktioner hittades
", +"
+

All dimensions in centimeter only

+
","
+

Alla mått endast i centimeter

+
", +"

About Product Bundle

+ +

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.

+

The package Item will have Is Stock Item as No and Is Sales Item as Yes.

+

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.

","

Om Artikel Paket

+ +

Paketera grupp av artiklar till annan Artikel. Det är användbart om man paketerar vissa Artiklar i paket och har lager av packade Artiklar och inte paketarad Artikel.Artikel kommer att ha Artikel. Är Lagerar Artikel som Nej och Är Försäljning Artikel som Ja.

+

Exempel:

+

Om man säljer bärbara datorer och ryggsäckar separat och har specialpris om kunder köper båda, så kommer det att vara bärbar dator + ryggsäck som paket artikel.", +"

Currency Exchange Settings Help

+

There are 3 variables that could be used within the endpoint, result key and in values of the parameter.

+

Exchange rate between {from_currency} and {to_currency} on {transaction_date} is fetched by the API.

+

Example: If your endpoint is exchange.com/2021-08-01, then, you will have to input exchange.com/{transaction_date}

","

Valutaväxling Inställningar Hjälp

+

Det finns 3 variabler som kan användas av slutpunkt, resultat nyckel och i parameter värde.

+

Växelkurs mellan {from_currency} och {to_currency} {transaction_date} hämtas av API.

+

Exempel: Om slutpunkt är exchange.com/2021-08-01 måste du ange exchange.com/{transaction_date}

", +"

Body Text and Closing Text Example

+ +
We have noticed that you have not yet paid invoice {{sales_invoice}} for {{frappe.db.get_value(""Currency"", currency, ""symbol"")}} {{outstanding_amount}}. This is a friendly reminder that the invoice was due on {{due_date}}. Please pay the amount due immediately to avoid any further dunning cost.
+ +

How to get fieldnames

+ +

The fieldnames you can use in your template are the fields in the document. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Sales Invoice)

+ +

Templating

+ +

Templates are compiled using the Jinja Templating Language. To learn more about Jinja, read this documentation.

","

Huvudtext och exempel på Avslutande Text

+ +
Vi har märkt att ni ännu inte har betalat faktura {{sales_invoice}} för {{frappe.db.get_value(""Currency"", currency, ""symbol"")}} {{outstanding_amount}}. Detta är en vänlig påminnelse om att fakturan var förfallen {{due_date}}. Vänligen betala förfallen belopp omedelbart för att undvika ytterligare kostnader.
+ +

Hur får man fältnamn

+ +

Fältnamn man kan använda i mall är fält i dokument. Du kan ta reda på fält namn för alla dokument via Inställningar > Anpassa formulärvy och välj dokument typ (t.ex. Försäljning Faktura)

+ +

Mall

+ +

Mallar kompileras med Jinja Templating Language. Om du vill veta mer om Jinja läs denna dokumentation.

", +"

Contract Template Example

+ +
Contract for Customer {{ party_name }}
+
+-Valid From : {{ start_date }} 
+-Valid To : {{ end_date }}
+
+ +

How to get fieldnames

+ +

The field names you can use in your Contract Template are the fields in the Contract for which you are creating the template. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Contract)

+ +

Templating

+ +

Templates are compiled using the Jinja Templating Language. To learn more about Jinja, read this documentation.

","

Exempel på Avtal Mall

+ +
Avtal för Kund {{ party_name }}
+
+-Giltigt från: {{ start_date }}
+-Gäller till: {{ end_date }}
+
+ +

Hur får man fältnamn

+ +

Fältnamn du kan använda i avtal mall är fält i avtal som du skapar mallen för. Du kan ta reda på fält namn för alla dokument via Inställningar > Anpassa formulärvy och välj dokument typ (t.ex. Avtal)

+ +

Mall

+ +

Mallar kompileras med Jinja Templating Language. Om du vill veta mer om Jinja läser du den här dokumentationen.

", +"

Standard Terms and Conditions Example

+ +
Delivery Terms for Order number {{ name }}
+
+-Order Date : {{ transaction_date }} 
+-Expected Delivery Date : {{ delivery_date }}
+
+ +

How to get fieldnames

+ +

The fieldnames you can use in your email template are the fields in the document from which you are sending the email. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Sales Invoice)

+ +

Templating

+ +

Templates are compiled using the Jinja Templating Language. To learn more about Jinja, read this documentation.

","

Exempel på Standard Villkor

+ +
Leverans Villkor för Order Nummer {{ name }}
+
+-Order Datum: {{ transaction_date }}
+-Förväntat Leverans Datum: {{ delivery_date }}
+
+ +

Hur hämtas fältnamn

+ +

Fältnamn som kan användas i E-post Mall är fält i Dokument som man skickar E-post meddelande från. Man kan ta reda på fält namn för alla dokument via Inställning > Anpassa Formulär Vy och välja Dokument Typ (t.ex. Försäljning Faktura)

+ +

Skriva Mallar

+ +

Mallar kompileras med Jinja Mall Språk. Läs mer om Jinja dokumentation:

", +"
Or
","
Eller
", +"","", +"","", +"","", +"

In your Email Template, you can use the following special variables: +

+
    +
  • + {{ update_password_link }}: A link where your supplier can set a new password to log into your portal. +
  • +
  • + {{ portal_link }}: A link to this RFQ in your supplier portal. +
  • +
  • + {{ supplier_name }}: The company name of your supplier. +
  • +
  • + {{ contact.salutation }} {{ contact.last_name }}: The contact person of your supplier. +
  • + {{ user_fullname }}: Your full name. +
  • +
+

+

Apart from these, you can access all values in this RFQ, like {{ message_for_supplier }} or {{ terms }}.

","

I E-post Mall kan följande specialvariabler användas: +

+
    +
  • + {{ update_password_link }} : Länk där leverantör kan ange nytt lösenord för att logga in på leverantör portal. +
  • +
  • + {{ portal_link }}: Länk till offert begäran på leverantör portal. +
  • +
  • + {{ supplier_name }}: Leverantör namn. +
  • +
  • + {{ contact.salutation }} {{ contact.last_name }}: Kontakt person för leverantör. +
  • +
  • + {{ user_fullname } }: Användarens fullständiga namn. +
  • +
+

+

+

Förutom dessa kan man komma åt alla värden i offert begäran, som {{ message_for_supplier }} eller {{ terms }}.

", +"
Message Example
+ +<p> Thank You for being a part of {{ doc.company }}! We hope you are enjoying the service.</p> + +<p> Please find enclosed the E Bill statement. The outstanding amount is {{ doc.grand_total }}.</p> + +<p> We don't want you to be spending time running around in order to pay for your Bill.
After all, life is beautiful and the time you have in hand should be spent to enjoy it!
So here are our little ways to help you get more time for life! </p> + +<a href=""{{ payment_url }}""> click here to pay </a> + +
+","
Meddelande Exempel
+ +<p> Tack för att ni är en del av {{ doc.company }}! Vi hoppas att ni gillar tjänst.</p> + +<p> Vänligen se bifogat E-faktura. Utestående belopp är {{ doc.grand_total }}.</p> + +<p> Vi vill inte att ni ska spendera tid med att springa runt för att betala faktura.
Livet är trots allt vackert och den tid man har bör spenderas för att njuta av livet!
Så här är våra små sätt att hjälpa er att få mer tid för livet! < /p> + +<a href=""{{ payment_url }}""> klicka här för att betala </a> + +
+", +"
Message Example
+ +<p>Dear {{ doc.contact_person }},</p> + +<p>Requesting payment for {{ doc.doctype }}, {{ doc.name }} for {{ doc.grand_total }}.</p> + +<a href=""{{ payment_url }}""> click here to pay </a> + +
+","
Meddelande Exempel
+ +<p>Hej {{ doc.contact_person }},</p> + +<p>Begär betalning för {{ doc.doctype }}, {{ doc.name }} för {{ doc.grand_total }}.</p> + +<a href=""{{ payment_url }}""> klicka här för att betala </a> + +
+", +"Masters & Reports","Inställningar & Rapporter", +"Quick Access","Genvägar", +"Reports & Masters","Rapporter & Inställningar", +"Reports & Masters","Rapporter & Inställningar", +"Settings","Inställningar", +"Shortcuts","Genvägar", +"Your Shortcuts + + + + + + ","Genvägar + + + + + + ", +"Your Shortcuts","Genvägar", +Grand Total: {0},Totalt: {0}, +Outstanding Amount: {0},Utestående belopp: {0}, +" + + + + + + + + + + + + + + + + + +
Child DocumentNon Child Document
+

To access parent document field use parent.fieldname and to access child table document field use doc.fieldname

+ +
+

To access document field use doc.fieldname

+
+

Example: parent.doctype == ""Stock Entry"" and doc.item_code == ""Test""

+ +
+

Example: doc.doctype == ""Stock Entry"" and doc.purpose == ""Manufacture""

+
+ + + + + + +"," + + Underordnad Dokument + Överordnad Dokument + + + + + +

För att komma åt överordnad dokument fält använd parent.fieldname och för att komma åt underordnad dokument fält använd doc.fieldname

+ + + +

För att komma åt dokument fält använd doc.fieldname

+ + + + +

Exampel: parent.doctype == ""Stock Entry"" and doc.item_code == ""Test""

+ + + +

Exampel: doc.doctype == ""Stock Entry"" and doc.purpose == ""Produktion""

+ + + + + + + + + + + +", +A Holiday List can be added to exclude counting these days for the Workstation.,Helg Lista kan läggas till för att utesluta dessa dagar för Arbetsstation., +A Packing Slip can only be created for Draft Delivery Note.,Packsedel kan endast skapas för utkast till Försäljning Följesedel., +"A Price List is a collection of Item Prices either Selling, Buying, or both","Prislista är samling av artikel priser som antingen säljs, köpes eller båda", +A Reconciliation Job {0} is running for the same filters. Cannot reconcile now,Avstämning jobb {0} körs för samma filter. Kan inte stämma av nu, +A Transaction Deletion Document: {0} is triggered for {0},Transaktion Borttagning jobb utlöst för {0} , +A customer must have primary contact email.,Kund måste ha primär kontakt e-post adress., +A driver must be set to submit.,Förare måste anges för att godkänna., +A template with tax category {0} already exists. Only one template is allowed with each tax category,Mall med moms kategori {0} finns redan. Endast en mall är tillåten med varje moms kategori, +API Details,API Detaljer, +AWB Number,AWB Nummer, +Abbreviation: {0} must appear only once,Förkortning: {0} får endast visas en gång, +About Us Settings,Om Oss Inställningar, +About {0} minute remaining,Cirka {0} minut kvar, +About {0} minutes remaining,Cirka {0} minuter kvar, +About {0} seconds remaining,Cirka {0} sekunder kvar, +Acceptance Criteria Formula,Acceptans Kriterier Formel , +Acceptance Criteria Value,Acceptans Kriterier Värde, +Accepted Qty in Stock UOM,Godkänd Kvantitet (per Lager Enhet), +Access Key,Åtkomst Nyckel, +Access Key is required for Service Provider: {0},Åtkomst Nyckel erfordras för Tjänsteleverantör: {0}, +Account Balance (From),Konto Saldo (Från), +Account Balance (To),Konto Saldo (Till), +Account Closing Balance,Konto Stängning Saldo, +Account Currency (From),Konto Valuta (Från), +Account Currency (To),Konto Valuta (Till), +Account Opening Balance,Öppning Saldo, +Account not Found,Konto ej funnen, +Account {0} added multiple times,Konto {0} har lagts till flera gånger, +Accounting Dimension Filter,Bokföring Dimension Filter, +Accounting Dimensions Filter,Bokföring Dimension Filter, +Accounting Entry for {0},Bokföring Post för {0}, +Accounts Closing,Bokföring Låsning, +Accounts Missing Error,Konton Saknas Fel, +Accounts Receivable/Payable,Fordringar/Skulder, +Accounts to Merge,Konton som ska Slås Samman, +Action If Quality Inspection Is Rejected,Åtgärd om Kvalitet Kontroll är Avvisad, +Action If Same Rate is Not Maintained,Åtgärd om Marginal inte Bibehålls, +Action if Same Rate is Not Maintained Throughout Sales Cycle,Åtgärd om samma Pris inte bibehålls under Försäljning, +Active Status,Aktiv Status, +Actual Balance Qty,Faktiskt Saldo Kvantitet, +Actual Expense,Faktisk Kostnad, +Actual Posting,Faktisk Postning, +Actual Qty in Warehouse,Faktisk Kvantitet på Lager, +Actual Time,Verklig Tid, +Add Columns in Transaction Currency,Lägg till kolumner i Transaktion Valuta, +Add Corrective Operation Cost in Finished Good Valuation,Lägg till Korrigerande Driftkostnad i Färdig Artikel, +Add Discount,Lägg till Rabatt, +Add Items in the Purpose Table,Lägg till Artiklar i Syfte Tabell, +Add Lead to Prospect,Lägg till Potentiell Kund till Prospekt, +Add Local Holidays,Lägg till Lokal Helgdag, +Add Manually,Lägg till Manuellt, +Add Or Deduct,Lägg till eller Dra av, +Add Serial / Batch Bundle,Lägg till Serie / Parti Paket, +Add Serial / Batch No,Lägg till Serie/Parti Nummer, +Add Serial / Batch No (Rejected Qty),Lägg till Serie/Parti Nummer (Avvisad Kvantitet), +Add Stock,Lägg till Lager, +Add Sub Assembly,Lägg till Delmontering, +Add Template,Lägg till Mall, +Add a Note,Lägg till Notering, +Add details,Lägg till Detaljer, +Add to Prospect,Lägg till Prospekt, +Added By,Lagt till Av, +Added On,Tillagd, +Added Supplier Role to User {0}.,Lade till Leverantör Roll till Användare {0}., +Added {1} Role to User {0}.,Lade till {1} roll till användare {0}., +Adding Lead to Prospect...,Lägger till Potentiell Kund..., +Additional,Extra, +Additional Asset Cost,Extra Tillgång Kostnad, +Additional Cost Per Qty,Extra Kostnad per Kvantitet, +Additional Info,Extra Information, +Address And Contacts,Adress & Kontakter, +Adjust Asset Value,Justera Tillgång Värde, +Adjustment Against,Justering Mot, +Adjustment based on Purchase Invoice rate,Justering Baserad på Inköp Faktura Pris, +Advance Account: {0} must be in either customer billing currency: {1} or Company default currency: {2},Förskott Konto: {0} måste vara antingen i kundens fakturering valuta: {1} eller bolag standard valuta: {2}, +Advance Payment,Förskott Betalning, +Advance Tax,Förskott Moms, +Advance Taxes and Charges,Förskott Moms och Avgifter, +Advance paid against {0} {1} cannot be greater than Grand Total {2},Förskott Betalning mot {0} {1} kan inte vara större än Totalt Belopp {2}, +Advance payments allocated against orders will only be fetched,Förskott betalningar allokerade mot order kommer att hämtas, +Affected Transactions,Berörda Transaktioner, +Against Customer Order {0},Mot Kund Order {0}, +Against Supplier Invoice {0},Mot Leverantör Faktura {0}, +Against Voucher No,Mot Verifikat Nummer, +Age ({0}),Ålder ({0}), +Ageing Range,Åldring Intervall, +Agent Busy Message,Agent Upptaget Meddelande, +Agent Group,Agent Grupp, +Agent Unavailable Message,Agent Otillgänglig Meddelande, +Aggregate a group of Items into another Item. This is useful if you are maintaining the stock of the packed items and not the bundled item,Paketera flera Artiklar till ett annat Artikel. Användbart om lager baseras på packade artiklar och inte ingående artiklar, +Algorithm,Algoritm, +All Activities,Alla Aktivitet, +All Activities HTML,Alla Aktivitet HTML, +All Items,Alla Artiklar, +All Sales Transactions can be tagged against multiple Sales Persons so that you can set and monitor targets.,Alla försäljning transaktioner kan taggas mot flera säljare för att ange och övervaka mål., +All allocations have been successfully reconciled,Alla tilldelningar är avstämda, +All items have already been received,Alla Artiklar är redan mottagna, +All items in this document already have a linked Quality Inspection.,Alla Artiklar i detta dokument har redan länkad Kvalitet Kontroll., +All the Comments and Emails will be copied from one document to another newly created document(Lead -> Opportunity -> Quotation) throughout the CRM documents.,Alla kommentarer och E-post meddelande kommer att kopieras från ett dokument till ett annat nyskapad dokument (Potentiell Kund -> Möjlighet -> Försäljning Offert) genom hela Säljstöd process., +"All the required items (raw materials) will be fetched from BOM and populated in this table. Here you can also change the Source Warehouse for any item. And during the production, you can track transferred raw materials from this table.",Alla nödvändiga artiklar (råmaterial) kommer att hämtas från stycklista och läggs till denna tabell. Här kan du också ändra hämtlager för valfri artikel. Och under produktion kan du spåra överförd råmaterial från denna tabell., +Allocate Payment Request,Tilldela Betalning Begäran, +Allocated Entries,Tilldelade Poster, +Allocated To:,Tilldelad Till:, +Allocations,Tilldelningar, +Allow,Tillåt, +Allow Alternative Item must be checked on Item {},Tillåt Alternativ Artikel måste vara vald för Artikel {}, +Allow Continuous Material Consumption,Tillåt Kontinuerlig Material Förbrukning, +Allow Excess Material Transfer,Tillåt Överflödig Material Överföring, +Allow Internal Transfers at Arm's Length Price,Tillåt Interna Överföringar till Marknad Pris, +Allow Item to be Added Multiple Times in a Transaction,Tillåt att Artikel läggs till flera gånger i Transaktion, +Allow Lead Duplication based on Emails,Tillåt Potentiella Kunder Duplicering baserat på E-post Meddelande, +Allow Negative rates for Items,Tillåt Negativa Priser för Artiklar, +Allow Or Restrict Dimension,Tillåt eller Begränsa Dimension, +Allow Partial Reservation,Tillåt Partiell Reservation, +Allow Purchase,Tillåt Inköp, +Allow Sales,Tillåt Försäljning, +Allow Sales Order Creation For Expired Quotation,Tillåt att Försäljning Order skapas för Förfallen Försäljning Offert, +Allow User to Edit Discount,Tillåt Användare att Redigera Rabatt, +Allow User to Edit Rate,Tillåt Användare att Redigera Pris, +Allow Zero Rate,Tillåt Noll Pris, +Allow material consumptions without immediately manufacturing finished goods against a Work Order,Tillåt Material Förbrukning utan att omedelbart producera färdiga artiklar mot Arbetsorder, +Allow multi-currency invoices against single party account ,Tillåt Fler Valuta Fakturor mot Parti Konto, +Allow to Edit Stock UOM Qty for Purchase Documents,Tillåt att redigera Lager Enhet Kvantitet för Inköp Dokument, +Allow to Edit Stock UOM Qty for Sales Documents,Tillåt att redigera Lager Enhet Kvantitet för Försäljning Dokument, +Allow transferring raw materials even after the Required Quantity is fulfilled,Tillåt överföring av råmaterial även efter att Erfordrad Kvantitet är uppfylld, +Allowed,Tillåten, +Allowed Dimension,Tillåten Dimension, +Allowed Doctypes,Tillåtna Dokument, +Allowed Items,Tillåtna Artiklar, +Allowed primary roles are 'Customer' and 'Supplier'. Please select one of these roles only.,Tillåtna primära roller är 'Kund' och 'Leverantör'. Välj endast en av dessa roller., +Allows to keep aside a specific quantity of inventory for a particular order.,Tillåter reservation av specifierad artikel kvantitet för angiven order., +Already Picked,Redan Plockad, +Alternative Items,Alternativa Artiklar, +"Alternatively, you can download the template and fill your data in.",Alternativt kan du ladda ner mall och fylla i dina uppgifter., +Amount (AED),Belopp (AED), +Amount Eligible for Commission,Provision Belopp, +Amount in Account Currency,Belopp i Konto Valuta, +Amount in party's bank account currency,Belopp i partens Bank Konto Valuta, +Amount in transaction currency,Belopp i transaktion valuta, +An Item Group is a way to classify items based on types.,Artikel grupp är ett sätt att klassificera artiklar baserat på typer., +An error has been appeared while reposting item valuation via {0},Fel har uppstått vid ompostering av artikel värdering via {0}, +An error has occurred during {0}. Check {1} for more details,Fel har uppstått under {0}. Kontrollera {1} för mer information,Error Log +Annual Revenue,Årlig Omsätning, +"Another Cost Center Allocation record {0} applicable from {1}, hence this allocation will be applicable upto {2}","Annan Resultat Enhet Tilldelning Post {0} är tillämplig från {1}, därför kommer denna tilldelning att gälla upp till {2}", +"Any one of following filters required: warehouse, Item Code, Item Group","Något av följande filter erfordras: Lager, Artikelkod, Artikelgrupp", +Applicable Dimension,Tillämpad Dimension, +Applicable On Account,Tillämplig På Konto, +Applied on each reading.,Tillämpas vid varje läsning., +Applied putaway rules.,Tillämpad Läggundan Regler, +Apply Putaway Rule,Tillämpa Lägg Undan Regel, +Apply Recursion Over (As Per Transaction UOM),Tillämpa Rekursion Över (per Transaktion Enhet), +Apply SLA for Resolution Time,Tillämpa Service Nivå Avtal för Resolution Tid, +Apply TDS,Tillämpa TDS, +Apply Tax Withholding Amount ,Tillämpa Moms Avdrag Belopp, +Apply restriction on dimension values,Tillämpa begränsning på dimension värde, +Apply to All Inventory Documents,Tillämpa på Alla Lager Dokument, +Apply to Document,Tillämpa på Dokument, +Appointment Created Successfully,Möte Bokad!, +Appointment Scheduling Disabled,Bokning av Möten Inaktiverad, +Appointment Scheduling has been disabled for this site,Bokning av Möten är Inaktiverad för denna Webbplats, +Appointment was created. But no lead was found. Please check the email to confirm,Möte Skapad. Men inget Poteentiel Kund hittades. Kontrollera e-post meddelande för att bekräfta, +Approximately match the description/party name against parties,Ungefärlig avstämning av beskrivning/partinamn mot parti, +Are you sure you want to clear all demo data?,Är du säker på att du vill ta bort alla demodata?, +Are you sure you want to delete this Item?,Är du säker på att du vill ta bort detta Artikel?, +Are you sure you want to restart this subscription?,Är du säker på att du vill starta om denna prenumeration?, +As on Date,Som den, +"As there are existing submitted transactions against item {0}, you can not change the value of {1}.",Eftersom det finns befintliga godkäAda transaktioner mot artikel {0} kan man inte ändra värdet på {1}., +"As there are negative stock, you can not enable {0}.",Eftersom det finns negativ lager kan du inte aktivera {0}., +"As there are reserved stock, you cannot disable {0}.",Eftersom det finns reserverat lager kan du inte inaktivera {0}., +"As there are sufficient Sub Assembly Items, Work Order is not required for Warehouse {0}.",Eftersom det finns tillräckligt med Undermontering Artiklar erfordras inte Arbetsorder för Lager {0}., +"As {0} is enabled, you can not enable {1}.",Eftersom {0} är aktiverad kan du inte aktivera {1}., +Assembly Items,Montering Artiklar, +Asset Activity,Tillgång Aktivitet, +Asset Capitalization,Tillgång Bokning, +Asset Capitalization Asset Item,Tillgång Bokning Tillgång Post, +Asset Capitalization Service Item,Tillgång Bokning Service Post, +Asset Capitalization Stock Item,Tillgång Bokning Lager Post, +Asset Depreciation Details,Tillgång Avskrivning Detaljer, +Asset Depreciation Schedule,Tillgång Avskrivning Schema, +Asset Depreciation Schedule for Asset {0} and Finance Book {1} is not using shift based depreciation,Tillgång Avskrivning Schema för Tillgång {0} och Finans Register {1} använder inte skift baserad avskrivning, +Asset Depreciation Schedule not found for Asset {0} and Finance Book {1},Tillgång Avskrivning Schema finns inte för Tillgång {0} och Finans Register {1}, +Asset Depreciation Schedule {0} for Asset {1} already exists.,Tillgång Avskrivning Schema {0} för Tillgång {1} finns redan., +Asset Depreciation Schedule {0} for Asset {1} and Finance Book {2} already exists.,Tillgång Avskrivning Schema {0} för Tillgång {1} och Finans Register {2} finns redan., +"Asset Depreciation Schedules created:
{0}

Please check, edit if needed, and submit the Asset.",Tillgång Avskrivning Schema skapades:
{0}

Kontrollera och Godkänn Tillgång., +Asset ID,Tillgång ID, +Asset Quantity,Tillgång Antal, +Asset Repair Consumed Item,Tillgång Bokning Förbrukad Post, +Asset Settings,Tillgång Inställningar, +Asset Shift Allocation,Tillgång Skift Tilldelning, +Asset Shift Factor,Tillgång Förskjutning Faktor, +Asset Shift Factor {0} is set as default currently. Please change it first.,Tillgång Förskjutning Faktor {0} är för närvarande angiven som standard. Vänligen ändra det först., +Asset cancelled,Tillgång Annullerad, +Asset capitalized after Asset Capitalization {0} was submitted,Tillgång kapitaliserad efter att Tillgång Kapitalisering {0} godkändes , +Asset created,Tillgång Skapad, +Asset created after Asset Capitalization {0} was submitted,Tillgång skapad efter att Tillgång Kapitalisering {0} godkändes, +Asset created after being split from Asset {0},Tillgång skapad efter att ha delats från Tillgång {0}, +Asset decapitalized after Asset Capitalization {0} was submitted,Tillgång avkapitaliserad efter att Tillgång Kapitalisering {0} godkändes, +Asset deleted,Tillgång Borttagen, +Asset issued to Employee {0},Tillgång utfärdad till Personal {0}, +Asset out of order due to Asset Repair {0},Tillgång ur funktion på grund av reparation av Tillgång {0}, +Asset received at Location {0} and issued to Employee {1},Tillgång mottagen på plats {0} och utfärdad till Personal {1}, +Asset restored,Tillgång återställd, +Asset restored after Asset Capitalization {0} was cancelled,Tillgång återställd efter att Tillgång Kapitalisering {0} annullerats, +Asset returned,Tillgång återlämnad, +Asset scrapped,Tillgång skrotad, +Asset sold,Tillgång Såld, +Asset submitted,Tillgång Godkänd, +Asset transferred to Location {0},Tillgång överförd till Plats {0}, +Asset updated after being split into Asset {0},Tillgång uppdaterad efter att ha delats upp i Tillgång {0}, +Asset updated after cancellation of Asset Repair {0},Tillgång uppdaterad efter annullering av Tillgång Reparation {0}, +Asset updated after completion of Asset Repair {0},Tillgång uppdaterad efter slutförande av Tillgång Reparation {0}, +Asset {0} cannot be received at a location and given to an employee in a single movement,Tillgång {0} kan inte tas emot på plats och ges till Personal i en enda rörelse, +Asset {0} does not belong to Item {1},Tillgång {0} tillhör inte Post {1}, +Asset {0} does not exist,Tillgång {0} finns inte, +Asset {0} has been created. Please set the depreciation details if any and submit it.,Tillgång {0} skapad. Ange avskrivning detaljer och godkänn den., +Asset {0} has been updated. Please set the depreciation details if any and submit it.,Tillgång {0} uppdaterad. Ange avskrivning detaljer och godkänn den., +Asset's depreciation schedule updated after Asset Shift Allocation {0},Tillgång Avskrivning Schema uppdaterad efter Tillgång Förskjutning Tilldelning {0}, +Asset's value adjusted after cancellation of Asset Value Adjustment {0},Tillgång Värde Justerat efter annullering av Tillgång Värde Justering {0}, +Asset's value adjusted after submission of Asset Value Adjustment {0},Tillgångens Värde Justerat efter godkänade av Tillgång Värde Justering {0}, +Assign Job to Employee,Tilldela jobb till Personal, +Assignment,Tilldelning, +Assignment Conditions,Tilldelning Villkor, +At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} for the batch {4} in the warehouse {5}.,På rad #{0}: Plockad kvantitet {1} för artikel {2} är större än tillgänglig kvantitet {3} för parti {4} i lager {5}., +At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} in the warehouse {4}.,På rad #{0}: Plockad kvantitet {1} för artikel {2} är större än tillgänglig kvantitet {3} i lager {4}., +At row {0}: Batch No is mandatory for Item {1},Rad {0}: Parti Nummer erfordras för Artikel {1}, +At row {0}: Parent Row No cannot be set for item {1},Rad {0}: Överordnad rad nummer kan inte anges för artikel {1}, +At row {0}: Qty is mandatory for the batch {1},Rad {0}: Kvantitet erfordras för Artikel {1}, +At row {0}: Serial No is mandatory for Item {1},Rad {0}: Serie Nummer erfordras för Artikel {1}, +At row {0}: Serial and Batch Bundle {1} has already created. Please remove the values from the serial no or batch no fields.,Rad {0}: Serie och Parti Paket {1} år redan skapad. Ta bort värde från serienummer eller parti nummer fält., +At row {0}: set Parent Row No for item {1},Rad {0}: ange överordnad rad nummer för artikel {1}, +Attach CSV File,Bifoga CSV Fil, +Attendance & Leaves,Närvaro & Ledighet, +Attribute value: {0} must appear only once,Egenskap Värde: {0} får endast visas en gång, +Auto Create Exchange Rate Revaluation,Automatiskt Skapa Valuta Växling Kurs Omvärdering, +Auto Create Purchase Receipt,Automatiskt Skapa Inköp Följesedel, +Auto Create Serial and Batch Bundle For Outward,Automatiskt Skapa Serie Nummer och Parti Paket för Försäljning, +Auto Create Subcontracting Order, Automatiskt Skapa Underleverantör Order, +Auto Created Serial and Batch Bundle,Automatiskt Skapad Serie och Parti Paket, +Auto Creation of Contact,Automatiskt Skapa Kontakt, +Auto Email Report,Automatiskt E-post Rapport, +Auto Insert Item Price If Missing,Automatiskt Infoga Artikel Pris om det saknas, +Auto Name,Automatiskt, +Auto Reconcile,Automatiskt Avstämning, +Auto Reconcile Payments,Automatisk Betalning Avstämning, +Auto Reconciliation,Automatisk Avstämning, +Auto Reconciliation of Payments has been disabled. Enable it through {0},Automatisk Avstämning av Betalningar har inaktiverats. Aktivera genom {0}, +Auto Reserve Serial and Batch Nos,Automatisk Reservera Serie och Parti Nummer, +Auto Reserve Stock for Sales Order on Purchase,Automatisk Reservera Lager för Försäljning Order vid Inköp, +Auto close Opportunity Replied after the no. of days mentioned above,Automatiskt Stäng Besvarad Möjlighet efter ovan angivet antal dagar, +Auto match and set the Party in Bank Transactions,Automatiskt avstäm och ange Parti i Bank Transaktioner, +Auto write off precision loss while consolidation,Automatisk Avskrivning Precision av Förlust under Konsolidering, +Automatically Add Filtered Item To Cart,Automatiskt Lägg till Filtrerad Artikel till Kundkorg, +Automatically Fetch Payment Terms from Order,Automatikt Hämta Betalning Villkor från Order, +Automatically post balancing accounting entry,Automatiskt skapa balans bokföring post, +Available Batch Report,Tillgänglig Parti Rapport, +Available Qty For Consumption,Tillgänglig Kvantitet för Förbrukning, +Available Qty at Company,Tillgänglig Kvantitet, +Available Qty at Target Warehouse,Tillgänglig Kvantitet på Till Lager, +Available Qty to Reserve,Tillgängligt Kvantitet att Reservera, +Average Completion,Genomsnittlig Slutförande, +Avg Rate,Genomsnittlig Pris, +Avg Rate (Balance Stock),Genomsnittlig Pris (Lager Saldo), +BIN Qty,Lager Kvantitet, +BOM Created,Stycklista Skapad, +BOM Creator,Stycklista Generator, +BOM Creator Item,Stycklista Generator Post, +BOM Info,Stycklista Information, +BOM Level,Stycklista Nivå, +BOM Tree,Stycklista Träd, +BOM UoM,Stycklista Enhet, +BOM Update Batch,Stycklista Uppdatera Parti , +BOM Update Initiated,Stycklista Uppdatering Initierad, +BOM Update Log,Stycklista Uppdatering Logg, +BOM Update Tool Log with job status maintained,Stycklista Uppdatering Verktyg Logg med jobb status upprätthållen, +BOM Updation already in progress. Please wait until {0} is complete.,Stycklista Uppdatering pågår. Vänta tills {0} är klar., +BOM Updation is queued and may take a few minutes. Check {0} for progress.,Stycklista Uppdatering i kö och kan ta några minuter. Kontrollera {0} för framsteg., +BOM and Production,Stycklista & Produktion, +BOM recursion: {1} cannot be parent or child of {0},Stycklista Rekursion: {1} kan inte vara överordnad eller underordnad till {0}, +BOMs Updated,Stycklista Uppdaterad, +BOMs created successfully,Stycklista Skapad, +BOMs creation failed,Stycklista Skapande Misslyckades, +"BOMs creation has been enqueued, kindly check the status after some time",Skapandet av Stycklistor i Kö. Vänligen kontrollera status efter en tid, +Balance Qty (Stock),Saldo Kvantitet (Lager), +Balance Sheet Summary,Balans Rapport Översikt, +Balance Stock Value,Saldo Lager Värde, +Bank Reconciliation Tool,Bank Avstämning Verktyg, +Bank Statement Import,Bank Avstämning Import, +Bank Transaction {0} Matched,Bank Transaktion {0} avstämd, +Bank Transaction {0} added as Journal Entry,Bank Transaktion {0} har lagts till som Journal Post, +Bank Transaction {0} added as Payment Entry,Bank Transaktion {0} har lagts till som Betalning Post, +Bank Transaction {0} is already fully reconciled,Bank Transaktion {0} är redan helt avstämd, +Bank Transaction {0} updated,Bank Transaktion {0} uppdaterad, +Bank/Cash Account,Bank / Kassa Konto, +Bank/Cash Account {0} doesn't belong to company {1},Bank / Kassa Konto {0} tillhör inte bolag {1}, +Base Amount,Bas Belopp, +Base Cost Per Unit,Bas Kostnad per Enhet, +Base Rate,Bas Pris, +Base Tax Withholding Net Total,Bas Netto Totalt Ex Moms , +Base Total,Bas Totalt, +Base Total Billable Amount,Bas Totalt Fakturerbar Belopp, +Base Total Billed Amount,Bas Totalt Fakturerad Belopp, +Base Total Costing Amount,Bas Totalt Kostnad Belopp, +Based On Value,Baserad på Värde, +"Based on your HR Policy, select your leave allocation period's end date",Baserat på din Personal Princip välj slutdatum för din ledighet tilldelning, +"Based on your HR Policy, select your leave allocation period's start date",Baserat på din Personal Princip väljstart datum för ledighet period, +Batch Expiry Date,Parti Förfallodatum, +Batch No is mandatory,Parti Nummer erfordras, +Batch No {0} does not exists,Parti Nummer {0} finns inte, +Batch No {0} is linked with Item {1} which has serial no. Please scan serial no instead.,Parti Nummer {0} är länkat till Artikel {1} som har serie nummer. Skanna serie nummer istället., +Batch No.,Parti Nummer, +Batch Nos,Parti Nummer, +Batch Nos are created successfully,Parti Nummer Skapade, +Batch Not Available for Return,Parti Ej Tillgänglig för Retur, +Batch Qty,Parti Kvantitet, +Batch and Serial No,Parti och Serie Nummer, +Batch not created for item {} since it does not have a batch series.,Parti är inte skapad för Artikel {} eftersom den inte har Parti Nummer., +Batch {0} and Warehouse,Parti {0} och Lager, +Batch {0} is not available in warehouse {1},Parti {0} är inte tillgängligt i lager {1}, +Batchwise Valuation,Värdering per Parti, +Beginning of the current subscription period,Början av aktuell prenumeration period, +Below Subscription Plans are of different currency to the party default billing currency/Company currency: {0},Nedan Prenumeration Planer är i annan valuta än Parti standard valuta/bolag valuta: {0}, +Bill for Rejected Quantity in Purchase Invoice,Faktura för Avvisad Kvantitet i Inköp Faktura, +Billed Items To Be Received,Fakturerade Artiklar att Ta Emot, +"Billed, Received & Returned","Fakturerad,Mottagen & Returnerad", +Billing Address Details,Faktura Adress Detaljer, +Billing Interval in Subscription Plan must be Month to follow calendar months,Fakturering Intervall i Prenumeration Plan måste vara Månad för att följa kalender månader, +Bisect Accounting Statements,Halvera Bokföring Rapporter, +Bisect Left,Bisekt Vänster, +Bisect Nodes,Halvera Noder, +Bisect Right,Halvera Höger, +Bisecting From,Halvera Från, +Bisecting Left ...,Halverar Vänster..., +Bisecting Right ...,Halverar Höger..., +Bisecting To,Halverar Till, +Blanket Order Allowance (%),Blankoavtal Order Tillåtelse (%), +Bom No,Stycklista Nummer, +Book Advance Payments as Liability option is chosen. Paid From account changed from {0} to {1}.,Bokför Förskott Betalningar eftersom Skuld alternativ är vald. Betald från konto har ändrats från {0} till {1}., +Book Advance Payments in Separate Party Account,Bokför Förskott Betalningar på Separat Konto, +Book Tax Loss on Early Payment Discount,Bokför Moms Bortfall vid Tidig Betalning Rabatt, +Book an appointment,Boka Möte, +Booking stock value across multiple accounts will make it harder to track stock and account value.,Bokföring av Lager Värde på flera Konto gör det svårare att spåra lager och konto värde., +Books have been closed till the period ending on {0},Bokföring är låst till {0}, +Both Payable Account: {0} and Advance Account: {1} must be of same currency for company: {2},Både Skuld Konto: {0} och Förskott Konto: {1} måste vara i samma valuta för bolag: {2}, +Both Receivable Account: {0} and Advance Account: {1} must be of same currency for company: {2},Både Fordring Konto: {0} och Förskott Konto: {1} måste vara i samma valuta för bolag: {2}, +Both {0} Account: {1} and Advance Account: {2} must be of same currency for company: {3},Både {0} Konto: {1} och Förskott Konto: {2} måste vara i samma valuta för bolag: {3}, +Budget Exceeded,Budget Överskriden, +Buildable Qty,Producerbart Kvantitet, +Bulk Transaction Log,Mass Transaktion Logg, +Bulk Transaction Log Detail,Mass Transaktion Logg Detaljer, +Bulk Update,Mass Uppdatera, +Bundle Items,Paketera Artiklar, +Buying & Selling Settings,Inköp & Försäljning Inställningar, +Buying and Selling,Inköp & Försäljning, +"By default, the Supplier Name is set as per the Supplier Name entered. If you want Suppliers to be named by a Naming Series choose the 'Naming Series' option.","Som standard är leverantör namn satt enligt angiven Leverantörs Namn. Om man vill att leverantörer ska namnges av Nummer Serie Välj 'Nummer Serie'", +Bypass credit check at Sales Order,Ignorera Kredit Kontroll vid Försäljning Order, +COGS By Item Group,Kostnad för Sålda Artiklar Efter Artikel Grupp, +COGS Debit,Kostnad för Sålda Artiklar Debet, +CRM Note,Säljstöd Anteckning, +CRM Settings,Säljstöd Inställningar, +Calculate Product Bundle Price based on Child Items' Rates,Beräkna Artikel Paket Pris baserat på Underordnade Artiklar priser, +Calculate daily depreciation using total days in depreciation period,Beräkna daglig avskrivning med hjälp av totalt antal dagar i avskrivningsperiod, +Call Again,Ring Igen, +Call Ended,Samtal Avslutad, +Call Handling Schedule,Samtal Hantering Schema, +Call Received By,Samtal Mottagen Av, +Call Receiving Device,Samtal Mottagning Enhet, +Call Routing,Samtal Dirigering, +Call Schedule Row {0}: To time slot should always be ahead of From time slot.,Samtal Schema Rad {0}: Till tid bör alltid ligga före Från tid., +Call Type,Samtal Typ, +Callback,Återuppringning, +Campaign Item,Kampanj Artikel, +Can not close Work Order. Since {0} Job Cards are in Work In Progress state.,"Kan inte stänga Arbetsorder, eftersom {0} Jobbkort har Arbete pågår status.", +"Can not filter based on Child Account, if grouped by Account","Kan inte filtrera baserat på Underordnad Konto, om grupperat efter konto", +"Can't change the valuation method, as there are transactions against some items which do not have its own valuation method","Kan inte ändra värdering sätt, eftersom det finns transaktioner mot vissa artiklar som inte har egen värdering sätt", +Can't disable batch wise valuation for active batches.,Kan inte inaktivera partivis värdering för aktiva partier., +Can't disable batch wise valuation for items with FIFO valuation method.,Kan inte inaktivera partivis värdering för artiklar med FIFO värdering metod., +Cannot Merge,Kan inte Slå Samman, +Cannot Resubmit Ledger entries for vouchers in Closed fiscal year.,Kan inte godkänna om Register Poster för verifikationer under stängt Bokföringsår., +"Cannot amend {0} {1}, please create a new one instead.","Kan inte ändra {0} {1}, skapa ny istället.", +Cannot apply TDS against multiple parties in one entry,Kan inte tillämpa TDS mot flera parter i en post, +Cannot cancel as processing of cancelled documents is pending.,Kan inte avbryta eftersom behandling av annullerade dokument väntar., +Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet.,Kan inte annullera transaktion. Ompostering av artikel värdering vid godkännande är inte klar ännu., +Cannot change Reference Document Type.,Kan inte ändra Referens Dokument Typ, +Cannot complete task {0} as its dependant task {1} are not completed / cancelled.,Kan inte slutföra uppgift {0} eftersom dess beroende uppgift {1} inte har slutförts/avbrutits., +Cannot convert Task to non-group because the following child Tasks exist: {0}.,Kan inte konvertera uppgift till ej grupp eftersom följande underordnade uppgifter finns: {0}., +Cannot convert to Group because Account Type is selected.,Kan inte konvertera till Grupp eftersom Konto Typ är vald., +Cannot create Stock Reservation Entries for future dated Purchase Receipts.,Kan inte skapa Lager Reservation Poster för framtid daterade Inköp Följesedlar., +Cannot create a pick list for Sales Order {0} because it has reserved stock. Please unreserve the stock in order to create a pick list.,Kan inte skapa plocklista för Försäljning Order {0} eftersom den har reserverad lager. Vänligen avboka lager för att skapa plocklista., +Cannot create accounting entries against disabled accounts: {0},Kan inte skapa bokföring poster mot inaktiverade konto: {0}, +Cannot disable batch wise valuation for FIFO valuation method.,Kan inte inaktivera partivis värdering för FIFO värdering metod., +Cannot enqueue multi docs for one company. {0} is already queued/running for company: {1},Kan inte ställa flera dokument i kö för ett bolag. {0} är redan i kö/körs för bolag: {1}, +Cannot find a default warehouse for item {0}. Please set one in the Item Master or in Stock Settings.,Kan inte hitta standardlager för artikel {0}. Ange det i Artikelinställningar eller i Lagerinställningar., +Cannot make any transactions until the deletion job is completed,Kan inte skapa transaktioner förrän borttagning jobb är slutfört, +Cannot produce more item for {0},Kan inte producera fler artiklar för {0}, +Cannot produce more than {0} items for {1},Kan inte producera mer än {0} artiklar för {1}, +Cannot receive from customer against negative outstanding,Kan inte ta emot från kund mot negativt utestående, +Cannot retrieve link token for update. Check Error Log for more information,Kan inte hämta länk token för uppdatering Kontrollera Fellogg för mer information, +Cannot retrieve link token. Check Error Log for more information,Kan inte hämta länk token. Se fellogg för mer information, +Cannot {0} from {2} without any negative outstanding invoice,Kan inte {0} från {2} utan någon negativ utestående faktura, +Capacity (Stock UOM),Kapacitet (Lager Enhet), +Capacity in Stock UOM,Kapacitet i Lager Enhet, +Capacity must be greater than 0,Kapacitet måste vara högre än 0, +Capitalization,Kapitalisering, +Capitalization Method,Kaptitalisering Sätt, +Capitalize Asset,Kapitalisera Tillgång, +Capitalize Repair Cost,Kapitalisera Reparation Kostnad, +Capitalized,Kapitaliserad, +Carrier,Transportör, +Carrier Service,Transportör Service, +Carry Forward Communication and Comments,Vidarebefordra E-post och Kommentarer, +Category Details,Kategori Detaljer, +Caution: This might alter frozen accounts.,Varning: Detta kan ändra låsta konto., +Change in Stock Value,Förändring i Lager Värde, +Changed customer name to '{}' as '{}' already exists.,Ändrade kund namn till '{}' eftersom '{}' redan finns., +Changes,Ändringar, +Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount,"Debitering av typ ""Verklig"" i rad {0} kan inte inkluderas i Artikel Pris eller Betald Belopp", +Chart Of Accounts,Kontoplan, +Checked On,Kontrollerad, +Checking this will round off the tax amount to the nearest integer,Om vald avrundas moms belopp till närmaste heltal, +Cheques and Deposits Incorrectly cleared,Checkar och Depositioner felaktigt avstämda, +Choose a WIP composite asset,Välj pågående arbete Sammansatt Tillgång, +Clear Demo Data,Tar Bort Demo Data... , +Clear Notifications,Rensa Noteringar, +Clearing Demo Data...,Tar Bort Demo Data..., +Click on 'Get Finished Goods for Manufacture' to fetch the items from the above Sales Orders. Items only for which a BOM is present will be fetched.,"Klicka på ""Hämta Färdiga Artiklar för Produktion"" för att hämta artiklar från ovanstående Försäljning Ordrar. Endast artiklar för vilka det finns stycklista kommer att hämtas.", +Click on Add to Holidays. This will populate the holidays table with all the dates that fall on the selected weekly off. Repeat the process for populating the dates for all your weekly holidays,Klicka på 'Lägg till Helgdagar'. Detta kommer att fylla helg tabell med alla datum som infaller på valda veckovis ledighet. Upprepa processen för att fylla i datum för alla helgdagar, +Click on Get Sales Orders to fetch sales orders based on the above filters.,Klicka på 'Hämta Försäljning Order' för att hämta Försäljning Ordrar baserade på ovanstående filter., +Click to add email / phone,Klicka på att lägga till e-post / telefon, +Close Replied Opportunity After Days,Stäng Besvarad Möjlighet Efter Dagar, +Closed Work Order can not be stopped or Re-opened,Stängd Arbetsorder kan inte stoppas eller öppnas igen, +Closing,Stänger, +Closing Balance as per Bank Statement,Stängning Saldo enligt Bank, +Closing Balance as per ERP, Stängning Saldo enligt System, +Closing Stock Balance,Stängning Lager Saldo, +Columns are not according to template. Please compare the uploaded file with standard template,Kolumner är inte enligt mall. Jämför uppladdad fil med standardmall, +Communication Channel,Kommunikation Kanal, +Company Address Display,Bolag Adress Visning, +Company Billing Address,Bolag Faktura Adress, +Company Details,Bolag Detaljer, +Company Shipping Address,Bolag Leverans Adress, +Company Tax ID,Org.Nr., +Company and Posting Date is mandatory,Bolag och Bokföring Datum erfordras, +Company is mandatory,Bolag Erfordras, +Company is mandatory for generating an invoice. Please set a default company in Global Defaults.,Bolag erfordras för att skapa faktura. Ange standard bolag i Standard Inställningar., +Company which internal customer represents,Bolag som intern kund representerar, +Company which internal customer represents.,Bolag som intern kund representerar., +Company which internal supplier represents,Bolag som intern leverantör representerar, +Company {0} is added more than once,Bolag{0} har lagts till mer än en gång, +Company {} does not exist yet. Taxes setup aborted.,Bolag {} finns inte ännu. Moms inställning avbröts., +Company {} does not match with POS Profile Company {},Bolag {} stämmer inte med Kassa Profil Bolag {}, +Competitor,Konkurrent, +Competitor Detail,Konkurrent Detaljer, +Competitor Name,Konkurrent Namn, +Competitors,Konkurrenter, +Complete Job,Slutför Jobb, +Complete Order,Slutför Order, +Completed On,Slutförd, +Completed On cannot be greater than Today,Klar datum kan inte vara senare än idag, +Completed Tasks,Antal Klara Uppgifter, +Completed Time,Klar Tid, +Completion Date can not be before Failure Date. Please adjust the dates accordingly.,Slutförande datum kan inte vara före fel datum. Justera datum därefter., +Conditional Rule,Villkor Regel, +Conditional Rule Examples,Villkor Regel Exempel, +Configure Product Assembly,Konfigurera Artikel Produktion, +Configure the action to stop the transaction or just warn if the same rate is not maintained.,Konfigurera åtgärd att stoppa transaktion eller varna om Marginal inte bibehålls., +Connections,Anslutningar, +Consider Entire Party Ledger Amount,Inkludera Hela Partibok Belopp, +Consider Minimum Order Qty,Inkludera Minimum Order Kvantitet, +Consider Rejected Warehouses,Inkludera Avvisad Lager , +Considered In Paid Amount,Moms Inkluderad i Betald Belopp, +Consolidate Sales Order Items,Konsolidera Försäljning Order Artiklar, +Consolidate Sub Assembly Items,Konsolidera Delmontering Artiklar, +Consumed Asset Items is mandatory for Decapitalization,Förbrukade Tillgång Artiklar erfordras för Dekapitalisering, +Consumed Asset Total Value,Förbrukad Tillgång Totalt Värde, +Consumed Assets,Förbrukade Tillgångar, +Consumed Quantity,Förbrukad Kvantitet, +Consumed Stock Items,Förbrukade Lager Artiklar, +Consumed Stock Items or Consumed Asset Items are mandatory for creating new composite asset,Förbrukade Lager Artiklar eller Förbrukade Tillgång Artiklar erfordras för att skapa ny sammansatt tillgång, +"Consumed Stock Items, Consumed Asset Items or Consumed Service Items is mandatory for Capitalization","Förbrukade Lager Artiklar, Förbrukade Tillgång Artiklar eller Förbrukade Service Artiklar erfordras för Kapitalisering", +Consumed Stock Total Value,Förbrukad Lager Totalt Värde, +Consumption Rate,Förbrukning Värde, +Contact Details,Kontakt Uppgifter, +Contact Mobile,Kontakt Mobil, +Contact Us Settings,Kontakta Oss Inställningar, +Contacts,Kontakter, +Contract Template Help,Avtal Mall Hjälp, +Contribution Qty,Bidrag Kvantitet, +Control Historical Stock Transactions,Kontroll Tidigare Lager Transaktioner, +Conversion factor for item {0} has been reset to 1.0 as the uom {1} is same as stock uom {2}.,"Konvertering faktor för artikel {0} är återställd till 1,0 eftersom enhet {1} är samma som lager enhet {2}.", +Convert Item Description to Clean HTML in Transactions,Konvertera Artikel Beskrivning till ren HTML i Transaktioner, +Convert to Group,Konvertera till Grupp,Warehouse +Convert to Item Based Reposting,Konvertera till Artikel Baserad Ompostering, +Convert to Ledger,Konvertera till Register,Warehouse +Core,System, +Corrective Job Card,Korrigerande Jobbkort, +Corrective Operation,Korrigerande Åtgärd, +Corrective Operation Cost,Korrigerande Åtgärd Kostnad, +Cost Center Allocation,Resultat Enhet Tilldelning, +Cost Center Allocation Percentage,Resultat Enhet Procentuell Tilldelning , +Cost Center Allocation Percentages,Resultat Enhet Procentuell Tilldelning, +Cost Center For Item with Item Code {0} has been Changed to {1},Resultat Enhet för Artikel med Artikelkod {0} har ändrats till {1}, +"Cost Center is a part of Cost Center Allocation, hence cannot be converted to a group",Resultat Enhet är del av Resultat Enhet Tilldelning och kan därför inte konverteras till grupp, +Cost Center with Allocation records can not be converted to a group,Resultat Enhet med tilldelning poster kan inte konverteras till grupp, +Cost Center {0} cannot be used for allocation as it is used as main cost center in other allocation record.,Resultat Enhet {0} kan inte användas för tilldelning eftersom det används som Huvud Resultat Enhet i annan tilldelning post., +Cost Center {} doesn't belong to Company {},Resultat Enhet {} tillhör inte bolag {}, +Cost Center {} is a group cost center and group cost centers cannot be used in transactions,Resultat Enhet {} är Grupp Resultat Enhet och Grupp Resultat Enhet kan inte användas i transaktioner, +Cost Configuration,Kostnad Inställning, +Cost Per Unit,Kostnad Per Enhet, +Cost of New Capitalized Asset,Kostnad för ny Kapitaliserad Tillgång, +Cost of Poor Quality Report,Kostand för Dålig Kvalitet Rapport, +Cost to Company (CTC),Totalt Kostnad per År, +Costing Details,Kostnad Detaljer, +Could Not Delete Demo Data,Kunde inte ta bort demodata, +Could not auto update shifts. Shift with shift factor {0} needed.,Kunde inte uppdatera förskjutning automatiskt. Förskjutning med förskjutning faktor {0} behövs., +Could not detect the Company for updating Bank Accounts,Kunde inte identifiera bolag för uppdatering av Bank Konto, +Could not find path for ,Kunde inte hitta sökväg för, +Count,Antal, +Create Depreciation Entry,Skapa Avskrivning Post, +Create Employee records.,Skapa Personal Uppgifter, +Create Grouped Asset,Skapa Grupperad Tillgång, +Create Job Card based on Batch Size,Skapa Jobbkort baserad på Parti Storlek, +Create Journal Entries,Skapa Journal Poster, +Create Ledger Entries for Change Amount,Skapa Register Poster för Växel Belopp, +Create Link,Skapa Länk, +Create Multi-level BOM,Skapa Flernivå Stycklista, +Create New Customer,Skapa Ny Kund, +Create Opportunity,Skapa Möjlighet, +Create Prospect,Skapa Prospekt, +Create Reposting Entries,Skapa Ompostering Poster, +Create Reposting Entry,Skapa Ompostering Post, +Create Stock Entry,Skapa Lager Post, +Create Workstation,Skapa Arbetsplats, +Create a new composite asset,Skapa ny Sammansatt Tillgång, +Create a variant with the template image.,Skapa variant med Mall Bild., +Create in Draft Status,Skapa i Utkast Status, +Create {0} {1} ?,Skapa {0} {1} ?, +Created On,Skapad, +Created {0} scorecards for {1} between:,Skapade {0} Resultatkort för {1} mellan:, +Creating Delivery Note ...,Skapar Försäljning Följesedel ..., +Creating Journal Entries...,Skapar Journal Poster..., +Creating Packing Slip ...,Skapar Packsedel ..., +Creating Purchase Invoices ...,Skapar Inköp Ordrar ..., +Creating Purchase Receipt ...,Skapar Inköp Följesedel ..., +Creating Sales Invoices ...,Skapa Försäljning Fakturor ..., +Creating Stock Entry,Skapar Lager Post...., +Creating Subcontracting Order ...,Skapar Underleverantör Order ..., +Creating Subcontracting Receipt ...,Skapar Underleverantör Följesedel ..., +Creating User...,Skapar Användare..., +Creation,Skapande, +Creation of {1}(s) successful,Skapande av {1}(s) klar, +"Creation of {0} failed. + Check Bulk Transaction Log","Skapande av {0} misslyckad. + Kontrollera Mass Transaktion Logg", +"Creation of {0} partially successful. + Check Bulk Transaction Log","Skapande av {0} delvis klar. + Kontrollera Mass Transaktion Logg", +Credit (Transaction),Kredit (Transaktion), +Credit Amount in Transaction Currency,Kredit Belopp i Transaktion Valuta, +Credit Limit Crossed,Kredit Gräns Överskriden, +Credit Limit Settings,Kredit Gräns Inställningar, +"Credit Note will update it's own outstanding amount, even if ""Return Against"" is specified.","Kredit Nota kommer att uppdatera sitt eget utestående belopp, även om ""Retur Mot"" anges.", +Currency Exchange Settings Details,Valuta Växling Inställning Detaljer, +Currency Exchange Settings Result,Valuta Växling Inställning Resultat, +Current Asset,Aktuell Tillgång, +Current Index,Aktuellt Index, +Current Level,Aktuell Nivå, +Current Liability,Aktuell Skuld, +Current Node,Aktuell Nod, +Current Serial / Batch Bundle,Aktuell Serie / Parti Paket, +Custom,Anpassad, +Custom delimiters,Anpassade Avgränsare, +Customer ,Kund, +Customer / Item / Item Group,Kund / Artikel / Artikel Grupp, +Customer Defaults,Kund Standard, +Customer Group Item,Kund Grupp Artikel, +Customer Group: {0} does not exist,Kund Grupp: {0} finns inte, +Customer Item,Kund Artikel, +Customer Name: ,Kund Namn:, +Customer Portal Users,Portal  Användare, +Customer: ,Kund:, +Daily Time to send,Daglig Tid att Skicka, +Dashboard,Översikt Panel, +Data Based On,Data Baserad På, +Date ,Datum , +Date must be between {0} and {1},Datum måste vara mellan {0} och {1}, +Days before the current subscription period,Dagar före aktuell prenumeration period, +DeLinked,Bortkoppplad, +Deal Owner,Ansvarig, +Debit (Transaction),Debet (Transaktion), +Debit Amount in Transaction Currency,Debet Belopp i Transaktion Valuta, +"Debit Note will update it's own outstanding amount, even if ""Return Against"" is specified.","Debet Nota kommer att uppdatera sitt eget utestående belopp, även om ""Retur Mot"" anges.", +Debit-Credit Mismatch,Debet-Kredit överensstämmer ej, +Debit-Credit mismatch,Debet-Kredit överensstämmer ej, +Decapitalization,Dekapitalisering, +Decapitalized,Dekapitaliserad, +Default Advance Account,Standard Förskött Konto, +Default Advance Paid Account,Standard Förskött Skuld Konto, +Default Advance Received Account,Standard Förskött Intäkt Konto, +Default BOM not found for FG Item {0},Standard Stycklista hittades inte för Färdig Artikel {0}, +Default Discount Account,Standard Rabatt Konto, +Default In-Transit Warehouse,Standard I Transit Lager, +Default Operating Cost Account,Standard Driftskostnad Konto, +Default Payment Discount Account,Standard Rabatt Konto, +Default Provisional Account,Standard Provisorisk Konto, +Default Service Level Agreement for {0} already exists.,Standard Service Nivå Avtal för {0} finns redan., +Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You need to either cancel the linked documents or create a new Item.,Standard Enhet för Artikel {0} kan inte ändras eftersom det finns några transaktion(er) med annan Enhet. Man måste antingen annullera länkade dokument eller skapa ny artikel., +Default settings for your stock-related transactions,Standard inställningar för lager relaterade transaktioner, +"Default tax templates for sales, purchase and items are created.","Standard Moms Mallar för Försäljning,Inköp och Artiklar är skapade. ", +Deferred Accounting,Uppskjuten Bokföring, +Deferred Accounting Defaults,Uppskjuten Bokföring Standard, +Deferred Revenue and Expense,Uppskjuten Intäkt och Kostnad, +Deferred accounting failed for some invoices:,Uppskjuten Bokföring misslyckades för vissa fakturor:, +Delay (In Days),Försening (I Dagar), +Delayed,Försenad, +Delayed Tasks Summary,Försenad Uppgifter Översikt, +Delete Accounting and Stock Ledger Entries on deletion of Transaction,Ta bort Bokföring och Lager Register Poster vid radering av Transaktion, +Delete Bins,Ta bort från Papperskorg, +Delete Cancelled Ledger Entries,Ta bort Annullerade Register Poster, +Delete Dimension,Ta bort Dimension, +Delete Leads and Addresses,Ta bort Prospekt och Adresser, +Delete Transactions,Ta bort Transaktioner, +Deleted Documents,Papperskorg, +Deletion in Progress!,Borttagning Pågår!, +Delimiter options,Avgränsning Alternativ, +Delivery Manager,Leverans Ansvarig, +Delivery Note Packed Item,Försäljning Följesedel Packad Artikel, +Delivery Note(s) created for the Pick List,Försäljning Följesedel skapad för Plocklista, +Delivery User,Leverans Användare, +Delivery to,Leverera Till, +Demo Company,Demo Bolag, +Demo data cleared,Demo Data Borttagen, +Dependant SLE Voucher Detail No,Beroende SLE Verifikat Detalj Nummer, +Dependent Task {0} is not a Template Task,Beroende Uppgift {0} är inte Mall Uppgift, +Deposit,Insättning, +Depreciate based on daily pro-rata,Skriv av baserat på dagliga proportioner, +Depreciate based on shifts,Skriv av baserat på förskjutningar, +Depreciation Details,Avskrivning Detaljer, +Depreciation Entry Posting Status,Avskrivning Post Bokföring Status, +Depreciation Expense Account should be an Income or Expense Account.,Kostnad Avskrivning Konto ska vara Intäkt eller Kostnad Konto., +Depreciation Posting Date cannot be before Available-for-use Date,Avskrivning Bokföring Datum kan inte vara före Tillgänglig för Användning Datum, +Depreciation Row {0}: Depreciation Posting Date cannot be before Available-for-use Date,Avskrivning Rad {0}: Avskrivning Bokföring Datum kan inte vara före Tillgänglig för Användning Datum, +Depreciation Schedule View,Avskrivning Schema Vy, +Depreciation cannot be calculated for fully depreciated assets,Avskrivning kan inte beräknas för fullt avskrivna tillgångar, +Description of Content,Beskrivning av Innehåll, +Desk User,Skrivbord Användare, +Difference In,Differens I, +Difference Posting Date,Differens Bokföring Datum, +Difference Qty,Differens Kvantitet, +Different 'Source Warehouse' and 'Target Warehouse' can be set for each row.,Olika 'Från Lager' och 'Till Lager' kan anges för varje rad., +Dimension Details,Dimension Detaljer, +Dimension Filter Help,Dimension Filter Hjälp, +Dimension-wise Accounts Balance Report,Bokföring Saldo Rapport per Dimension, +Direct Expense,Direkta Kostnader, +Disable Last Purchase Rate,Inaktivera Senaste Inköp Pris, +Disable Serial No And Batch Selector,Inaktivera Serie Nummer och Parti Väljare, +Disabled Account Selected,Inaktiverad Konto Vald, +Disabled Warehouse {0} cannot be used for this transaction.,Inaktiverad Lager {0} kan inte användas för denna transaktion., +Disabled pricing rules since this {} is an internal transfer,Inaktiverade Prissättning Regler eftersom detta {} är intern överföring, +Disabled tax included prices since this {} is an internal transfer,Inaktiverade Pris Inklusive Moms eftersom detta {} är intern överföring, +Disables auto-fetching of existing quantity,Inaktiverar automatisk hämtning av befintlig kvantitet, +Disassemble,Demontera, +Disassemble Order,Demontering Order, +Discount Account,Rabatt Konto, +Discount Date,Rabatt Datum, +Discount Settings,Rabatt Inställningar, +Discount Validity,Rabatt Giltighet , +Discount Validity Based On,Rabatt Giltighet Baserad På, +Discount of {} applied as per Payment Term,Rabatt på {} tillämpad enligt Betalning Villkor, +Discounted Amount,Rabatterad Belopp, +"Discounts to be applied in sequential ranges like buy 1 get 1, buy 2 get 2, buy 3 get 3 and so on","Rabatter som ska tillämpas i sekventiella intervall som köp 1 få 1, köp 2 få 2, köp 3 få 3 och så vidare", +Discrepancy between General and Payment Ledger,Avvikelse mellan Bökföring Register och Betalning Register, +Dispatch Address,Leverans Adress, +Dispatch Address Name,Leverans Adress Namn, +Distinct Item and Warehouse,Distinkt Artikel och Lager, +Distribute Additional Costs Based On ,Fördela Extra Kostnader Baserat På, +Distribute Manually,Fördela Manuellt, +Do Not Explode,Utvidga Ej, +Do Not Update Serial / Batch on Creation of Auto Bundle,Uppdatera inte Serie / Parti vid skapande av Automatiskt Paket, +Do Not Use Batch-wise Valuation,Använd inte partivis värdering, +Do reposting for each Stock Transaction,Skapa ompostering för varje Lager Transaktion, +Do you still want to enable negative inventory?,Vill du fortfarande aktivera negativ Lager?, +DocField,DocType Fält, +DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it.,"DocTypes ska inte läggas till manuellt i ""Excluded DocTypes"" tabell . Man får bara ta bort poster från den.", +Document Type already used as a dimension,Dokument Typ används redan som dimension, +Documents,Dokument , +Documents: {0} have deferred revenue/expense enabled for them. Cannot repost.,Dokument: {0} har uppskjutna intäkter/kostnader aktiverat för dem. Kan inte posta om., +Domain Settings,Domän Inställningar, +Don't Reserve Sales Order Qty on Sales Return,Reservera inte Försäljning Order Kvantitet vid Försäljning Retur, +Don't Send Emails,Skicka inte E-post, +Dont Recompute tax,Räkna inte om Moms, +Download Backups,Säkerhetskopior, +Download CSV Template,Ladda ner CSV Mall, +Download Materials Request Plan,Ladda ner Material Begäran Plan, +Download Materials Request Plan Section,Ladda ner Material Begäran Plan Sektion, +Dunning Amount (Company Currency),Påminnelse Avgift (Bolag Valuta), +Dunning Level,Påminnelse Nivå, +Duplicate Closing Stock Balance,Kopiera Stängning Lager Saldo, +Duplicate Customer Group,Kopiera Kund Grupp, +Duplicate Finance Book,Kopiera Finans Register, +Duplicate Item Group,Kopiera Artikel Grupp, +Duplicate POS Invoices found,Kopiera Kassa Fakturor , +Dynamic Condition,Dynamiska Villkor, +Edit Capacity,Redigera Kapacitet, +Edit Cart,Ändra Kundkorg, +Edit Full Form,Öppna i Full Formulär, +Edit Note,Redigera Anteckning, +Editing {0} is not allowed as per POS Profile settings,Ej Tillåtet att Redigera {0} pga Kassa Profil Inställningar, +Either 'Selling' or 'Buying' must be selected,"""Inköp"" eller ""Försäljning"" måste väljas", +Email / Notifications,E-post / Aviseringar, +Email Address (required),E-post Adress (erfordras), +"Email Address must be unique, it is already used in {0}","E-post Adress måste vara unik, den används redan i {0}", +Email Digest Recipient,E-post Utskick Mottagare, +Email Digest: {0},E-post Utskick: {0}, +Email Domain,E-post Domän, +Email or Phone/Mobile of the Contact are mandatory to continue.,E-post eller Telefon / Mobil för Kontakt erfordras för att fortsätta., +Email verification failed.,E-post verifiering misslyckades., +Employee User Id,Användare ID, +Enable Allow Partial Reservation in the Stock Settings to reserve partial stock.,Aktivera Tillåt Partiell Reservation i Lager Inställningar för att reservera partiell lager., +Enable Automatic Party Matching,Aktivera Automatiskt Parti Avstämning, +Enable Common Party Accounting,Aktivera Gemensam Parti Bokföring, +Enable Discount Accounting for Selling,Aktivera Rabatt Bokföring för Försäljning, +Enable Fuzzy Matching,Aktivera Ungefärlig Avstämning, +Enable Health Monitor,Aktivera System Övervakning, +Enable Immutable Ledger,Aktivera Oförenderlig Bokföring, +Enable Provisional Accounting For Non Stock Items,Aktivera Provisorisk Bokföring för ej Lager Artiklar, +Enable Stock Reservation,Aktivera Lager Reservation, +Enable it if users want to consider rejected materials to dispatch.,Aktivera om användare vill inkludera att avvisat material ska skickas., +Enable this checkbox even if you want to set the zero priority,Aktivera denna kryssruta även om nollprioritet ska anges, +"Enable this option to calculate daily depreciation by considering the total number of days in the entire depreciation period, (including leap years) while using daily pro-rata based depreciation",Aktivera detta alternativ för att beräkna daglig avskrivning genom att använda totalt antal dagar i hela avskrivningsperiod (inklusive skottår) vid användning av proportionell baserad avskrivning, +Enable to apply SLA on every {0},Aktivera för att tillämpa Service Nivå Avtal på varje {0}, +Enabling this ensures each Purchase Invoice has a unique value in Supplier Invoice No. field within a particular fiscal year,Aktivera för att säkerställa att varje Inköp Faktura har unikt värde i fält Leverantör Faktura Nummer fält per bokföring år, +Enabling this option will allow you to record -

1. Advances Received in a Liability Account instead of the Asset Account

2. Advances Paid in an Asset Account instead of the Liability Account,Aktivera för att möjliggöra bokföring av:

1. Mottagna Förskott Betalningar bokförs på Skuld Konto istället för Tillgång Konto

2. Betalade Förskott Betalningar bokförs på Tillgång Konto istället för Skuld Konto, +Enabling this will allow creation of multi-currency invoices against single party account in company currency,Aktivera för att tillåta skapande av fakturor i flera valutor mot enskilt konto i bolag valuta, +Enabling this will change the way how cancelled transactions are handled.,Aktivering av detta ändrar hur avbrutna transaktioner hanteras., +End Transit,Avsluta Transit, +End of the current subscription period,Slut datum på aktuell prenumeration period, +"Enter First and Last name of Employee, based on Which Full Name will be updated. IN transactions, it will be Full Name which will be fetched.","Ange För och Efternamn på Personal, baserat på vilket Fullständigt namn kommer att uppdateras. Vid transaktioner kommer det att vara Fullständigt namn som kommer att hämtas.", +Enter Manually,Ange Manuellt, +Enter Serial Nos,Ange Serie Nummer, +Enter Visit Details,Ange Besök Detaljer, +Enter a name for Routing.,Ange namn för Åtgärd Följd., +"Enter a name for the Operation, for example, Cutting.","Ange namn för Åtgärd, till exempel Skärning.", +Enter a name for this Holiday List.,Ange namn för denna Helg Lista., +"Enter an Item Code, the name will be auto-filled the same as Item Code on clicking inside the Item Name field.","Ange Artikel Kod, namn kommer att automatiskt hämtas på samma sätt som Artikel Kod när man klickar i Artikel Namn fält .", +Enter each serial no in a new line,Ange varje Serie Nummer på ny rad, +"Enter the Operation, the table will fetch the Operation details like Hourly Rate, Workstation automatically. + + After that, set the Operation Time in minutes and the table will calculate the Operation Costs based on the Hourly Rate and Operation Time.","Ange Åtgärd, detaljer hämtas automatiskt som timpris, arbetsstation . + + Efteråt, anges Åtgärd tid i minuter och system beräknar Åtgärd Kostnad baserat på timpris och tid.", +Enter the opening stock units.,Ange Öppning Lager Enheter., +Enter the quantity of the Item that will be manufactured from this Bill of Materials.,Ange kvantitet för Artikel som ska produceras från denna Stycklista., +Enter the quantity to manufacture. Raw material Items will be fetched only when this is set.,Ange kvantitet som ska produceras. Råmaterial Artiklar hämtas endast när detta är angivet., +Error during caller information update,Fel uppstod under uppdatering av samtalsinformation, +Error while posting depreciation entries,Fel uppstod vid bokföring av avskrivning poster, +Error while processing deferred accounting for {0},Fel uppstod när uppskjuten bokföring för {0} bearbetades, +Error while reposting item valuation,Fel uppstod vid ombokning av artikel värdering , +"Error: This asset already has {0} depreciation periods booked. + The `depreciation start` date must be at least {1} periods after the `available for use` date. + Please correct the dates accordingly.","Fel: Denna tillgång har redan {0} avskrivning perioder bokade. + Start datum för ""avskrivning"" måste vara minst {1} perioder efter ""tillgänglig för användning"" datum. + Korrigera datum enligt detta.", +Errors Notification,Fel Avisering, +Even invoices with apply tax withholding unchecked will be considered for checking cumulative threshold breach,Även fakturor med tillämpa moms undanhållning omarkerad kommer att betraktas för att kontrollera kumulativ tröskel överträdelse, +Example URL,Exempel URL, +Example of a linked document: {0},Exempel på länkad dokument: {0}, +"Example: ABCD.##### +If series is set and Serial No is not mentioned in transactions, then automatic serial number will be created based on this series. If you always want to explicitly mention Serial Nos for this item. leave this blank.","Exempel:. ABCD ##### Om serie är angiven och Serie Nummer inte anges i transaktioner, skapas Serie Nummer automatiskt utifrån denna serie. Om man alltid vill ange serie nummer för denna artikel lämna det tomt.", +Example: Serial No {0} reserved in {1}.,Exempel: Serie Nummer {0} reserverad i {1}., +Excess Materials Consumed,Överskott Material Förbrukad, +Excess Transfer,Överskott Överföring, +Exchange Gain Or Loss,Valutaväxling Resultat, +Exchange Gain/Loss amount has been booked through {0},Valutaväxling Resultat Belopp har bokförts genom {0}, +Exchange Rate Revaluation Settings,Valutaväxling Kurs Omvärdering Inställningar, +Excluded DocTypes,Exkluderade DocTyper, +Exempt Supplies,Undantagna Leveranser, +Expected,Förväntad, +Expected Balance Qty,Förväntad Saldo Kvantitet, +Expected End Date should be less than or equal to parent task's Expected End Date {0}.,Förväntat Slut Datum ska vara tidigare än eller lika med Överordnade Uppgifters förväntade Slut Datum {0}., +Expected Stock Value,Förväntad Lager Värde, +Expected Time Required (In Mins),Förväntad Tid (I Minuter), +Expiry,BESTFÖRE, +Export Data,Data Export, +Export Errored Rows,Exportera Felaktiga Rader, +Export Import Log,Exportera Import Logg, +Extra Consumed Qty,Extra Förbrukad Kvantitet, +Extra Job Card Quantity,Extra Jobbkort Kvantitet, +FIFO Queue vs Qty After Transaction Comparison,FIFO Kö mot Kvantitet Efter Transaktion Jämförelse, +"FIFO Stock Queue (qty, rate)","FIFO Lager Kö (kvantitet, pris)", +FIFO/LIFO Queue,FIFO / LIFO Kö, +Failed Entries,Misslyckade Poster, +"Failed to erase demo data, please delete the demo company manually.","Misslyckades att ta bort demodata, vänligen radera demo bolag manuellt.", +Failed to post depreciation entries,Kunde inte bokföra avskrivning poster, +Failed to setup defaults for country {0}. Please contact support.,Misslyckades att ange standard inställningar för {0}. Kontakta support., +Failure,Fel, +Failure Description,Fel Beskrivning, +Fetch Based On,Hämta Baserad På, +Fetch Overdue Payments,Hämta Förfallna Fakturor, +Fetch Timesheet,Hämta Tidrapport, +Fetch Value From,Hämta Värde Från, +Fetching exchange rates ...,Hämtar växel kurs ..., +Filter by Reference Date,Filtrera efter Referens Datum, +Filter on Invoice,Filtrera Faktura, +Filter on Payment,Filtrera Betalning, +Filters missing,Filter saknas, +Final Product,Färdig Artikel, +Financial Ratios,Finansiell Nyckeltal, +Financial Reports,Rapporter, +Financial Year Begins On,Bokföringsår Start Datum, +Financial reports will be generated using GL Entry doctypes (should be enabled if Period Closing Voucher is not posted for all years sequentially or missing) ,Finansiella Rapporter kommer att genereras med hjälp av Bokföring Poster (ska vara aktiverat om Period Låsning Verifikat inte publiceras för alla år i följd eller saknas), +Finished Good BOM,Färdig Stycklista, +Finished Good Item,Färdig Artikel, +Finished Good Item Qty,Färdig Artikel Kvantitet, +Finished Good Item Quantity,Färdig Artikel Kvantitet, +Finished Good Item is not specified for service item {0},Färdig Artikel är inte specificerad för service artikel {0}, +Finished Good Item {0} Qty can not be zero,Färdig Artikel {0} kvantitet kan inte vara noll, +Finished Good Item {0} must be a sub-contracted item,Färdig Artikel {0} måste vara underleverantör artikel, +Finished Good Qty,Färdig Artikel Kvantitet, +Finished Good Quantity ,Färdig Artikel Kvantitet, +Finished Good UOM,Färdig Artikel Enhet, +Finished Good {0} does not have a default BOM.,Färdig Artikel {0} har ingen standard Stycklista., +Finished Good {0} is disabled.,Färdig Artikel {0} är inaktiverad., +Finished Good {0} must be a stock item.,Färdig Artikel {0} måste vara lager artikel., +Finished Good {0} must be a sub-contracted item.,Färdig Artikel {0} måste vara underleverantör artikel., +Finished Goods Based Operating Cost,Färdiga Artiklar Baserade Driftskostnader, +Finished Goods Item,Färdig Artikel Post, +Finished Goods Reference,Färdig Artikel Referens, +Finished Goods Value,Färdig Artikel Värde, +Finished Goods based Operating Cost,Färdiga Artiklar Baserade Driftskostnader, +Finished Item {0} does not match with Work Order {1},Färdig Artikel {0} stämmer inte med Arbetsorder {1}, +First Response Due,Första Respons, +First Response SLA Failed by {},Första Respons Service Nivå Avtal misslyckades efter {}, +Fixed Asset Defaults,Fasta Tillgångar, +Fixed Time,Fast Tid, +Floor,Yta, +Floor Name,Yta Namn, +For Item,För Artikel, +For Item {0} cannot be received more than {1} qty against the {2} {3},För Artikel {0} kan inte tas emot mer än {1} i kvantitet mot {2} {3}, +For Job Card,För Jobbkort, +For Operation,För Åtgärd, +"For Return Invoices with Stock effect, '0' qty Items are not allowed. Following rows are affected: {0}","För Retur Fakturor med Lager påverkan, '0' kvantitet artiklar är inte tillåtna. Följande rader påverkas: {0}", +For Work Order,För Arbetsorder, +For dunning fee and interest,För påminnelseavgift och ränta, +"For item {0}, rate must be a positive number. To Allow negative rates, enable {1} in {2}","För Artikel {0} pris måste vara positiv tal. Att tillåta negativa priser, aktivera {1} i {2}", +For quantity {0} should not be greater than allowed quantity {1},För Kvantitet {0} ska inte vara högre än tillåten kvantitet {1}, +"For the item {0}, the quantity should be {1} according to the BOM {2}.",För artikel {0} ska kvantitet vara {1} enligt stycklista {2}., +"For the {0}, no stock is available for the return in the warehouse {1}.",För {0} finns inget kvantitet tillgängligt för retur i lager {1}., +"For the {0}, the quantity is required to make the return entry",För {0} erfordras kvantitet för att skapa retur post, +Force-Fetch Subscription Updates,Hämta Prenumeration Uppdateringar, +Forecasting,Prognos, +Formula Based Criteria,Formel Baserade Kriterier, +Free Item Rate,Gratis Artikel Pris, +From Corrective Job Card,Från Korrigerande Jobbkort, +From Date and To Date are mandatory,Från Datum och Till Datum Erfodras, +From Date is mandatory,Från Datum Erfordras, +From Date: {0} cannot be greater than To date: {1},Från Datum: {0} kan inte vara senare än Till Datum: {1}, +From Delivery Date,Från Leverans Datum, +From Doctype,Från DocType, +From Due Date,Från Förfallo Datum, +From Opportunity,Från Möjlighet, +From Payment Date,Från Betalning Datum, +From Prospect,Från Prospekt, +From Reference Date,Från Referens Datum, +From Voucher Detail No,Från Verifikat Detalj Nummer, +From Voucher No,Från Verifikat Nummer, +From Voucher Type,Från Verifikat Typ, +From and To dates are required,Från och Till Datum Erfodras, +Full and Final Statement,Avgång Avtal, +GL Balance,Bokföring Register Saldo, +GL Entry Processing Status,Bokföring Register Bearbetning Status, +GL reposting index,Bokföring Register Ompostering Index, +Gain/Loss accumulated in foreign currency account. Accounts with '0' balance in either Base or Account currency,Resultat ackumulerad på konto i utländsk valuta. Konto med '0' saldo i antingen Bas eller Konto valuta, +Gain/Loss already booked,Resultat Bokförd, +Gain/Loss from Revaluation,Omvärdering Resultat, +General Ledger,Bokföring Register,Warehouse +General and Payment Ledger Comparison,Bokföring och Betalning Register Jämförelse, +General and Payment Ledger mismatch,Bokföring och Betalning Register Jämförelse , +Generate Closing Stock Balance,Skapa Stängning Lager Saldo, +Generate Demo Data for Exploration,Skapa Demo Data för att Utforska, +Generate E-Invoice,Skapa E-Faktura, +Generate Invoice At,Skapa Faktura , +Generated,Skapad, +Generating Preview,Skapar Förhandsvisning, +Get Allocations,Hämta Tilldelningar, +Get Customer Group Details,Hämta Kund Grupp Detaljer, +Get Finished Goods for Manufacture,Hämta Artiklar för Produktion, +Get Outstanding Orders,Hämta Utestående Ordrar, +Get Raw Materials Cost from Consumption Entry,Hämta Råmaterial Kostnad från Förbrukning Post, +Get Raw Materials for Purchase,Hämta Råmaterial för Inköp, +Get Raw Materials for Transfer,Hämta Råmaterial för Överföring, +Get Scrap Items,Hämta Rest Artiklar, +Get Stock,Hämta Lager, +Get Sub Assembly Items,Hämta Delmontering Artiklar, +Get Supplier Group Details,Hämta Leverantör Grupp Detaljer, +Get Timesheets,Hämta Tidrapporter, +Get stops from,Hämta Leverans Stopp från, +Getting Scrap Items,Hämta Rest Artiklar, +Give free item for every N quantity,Lämna gratis artikel för varje N kvantitet, +Go back,Tillbaka, +Go to {0} List,Till {0} Lista, +Goals,Mål, +Goods,Gods, +Grant Commission,Bevilja Provision, +Greeting Message,Hälsning Meddelande, +Gross Profit Percent,Brutto Resultat %, +Gross Purchase Amount Too Low: {0} cannot be depreciated over {1} cycles with a frequency of {2} depreciations.,Brutto Inköp Belopp för lågt: {0} kan inte skrivas av över {1} cykler med en frekvens på {2} avskrivningar., +Gross Purchase Amount should be equal to purchase amount of one single Asset.,Brutto Inköp Belopp ska vara lika med inköp belopp för enskild Tillgång., +Group Same Items,Sammanfoga lika Artikelrader, +Growth View,Tillväxt Vy, +Half-yearly,Halvår, +Handle Employee Advances,Hantera Personal Förskott, +Has Alternative Item,Har Alternativ Artikel, +Has Item Scanned,Har Artikel Skannad, +Has Priority,Har Prioritet, +Have Default Naming Series for Batch ID?,Har Standard Nummer Serie för Parti?, +Heatmap,Värme Karta, +Height (cm),Höjd (cm), +"Hello,","Hej,", +Helps you distribute the Budget/Target across months if you have seasonality in your business.,Hjälper vid fördelning av Budget/ Mål över månader om bolag har säsongsvariationer., +Here are the error logs for the aforementioned failed depreciation entries: {0},Här är felloggar för ovannämnda misslyckade avskrivning poster: {0}, +Here are the options to proceed:,Här är alternativ för att fortsätta:, +"Here, you can select a senior of this Employee. Based on this, Organization Chart will be populated.",Här kan du välja överordnad för Personal. Baserat på detta kommer organisation diagam att fyllas i., +"Here, your weekly offs are pre-populated based on the previous selections. You can add more rows to also add public and national holidays individually.",Här är veckovisa ledigheter angivna i förväg baserat på de tidigare val. Man kan lägga till fler rader för att även lägga till allmänna och nationella helgdagar individuellt., +"Hi,","Hej,", +Hide Images,Dölj Bilder, +Holiday Date {0} added multiple times,Helgdag {0} har lagts till flera gånger, +Hours Spent,Förbrukade Timmar, +How often should Project be updated of Total Purchase Cost ?,Hur ofta ska Projekt uppdateras baserat på Totalt Inköp Kostnad?, +Idle,Overksam, +"If Enabled - Reconciliation happens on the Advance Payment posting date
+If Disabled - Reconciliation happens on oldest of 2 Dates: Invoice Date or the Advance Payment posting date
+","Om Aktiverad - Avstämning sker på Förskott Betalning datum
+Om Inaktiverad - Avstämning sker på äldsta av 2 datum: Faktura datum eller Förskott Betalning datum
+", +"If an operation is divided into sub operations, they can be added here.",Om åtgärd är uppdelad i underåtgärder kan de läggas till här., +"If checked, Rejected Quantity will be included while making Purchase Invoice from Purchase Receipt.","Om vald, kommer avvisad kvantitet att inkluderas när Inköp Faktura skapas från Inköp Följesedel.", +"If checked, Stock will be reserved on Submit","Om vald, kommer Lager Reservation att skapas vid Godkänn", +"If checked, picked qty won't automatically be fulfilled on submit of pick list.","Om vald, kommer plockad kvantitet inte automatiskt att uppfyllas när plocklista godkänns.", +"If checked, the tax amount will be considered as already included in the Paid Amount in Payment Entry","Om vald, kommer moms belopp anses vara inkluderad i Betald Belopp i Betalning Post", +"If checked, we will create demo data for you to explore the system. This demo data can be erased later.","Om vald,kommer demo data skapas så att man kan utforska system. Dessa demo data kan raderas senare.", +If enabled then system will manufacture Sub-assembly against the Job Card (operation).,Om aktiverad kommer system att producera Underenheter mot Jobbkort (Åtgärd)., +If enabled then system won't apply the pricing rule on the delivery note which will be create from the pick list,Om aktiverat kommer systemet inte att tillämpa prisregel på följesedel som kommer att skapas från plocklista, +If enabled then system won't override the picked qty / batches / serial numbers.,Om aktiverad kommer system inte åsidosätta plockad kvantitet / partier / serie nummer., +"If enabled, a print of this document will be attached to each email","Om aktiverad, kommer utskrift av detta dokument att bifogas till varje e-post meddelande", +"If enabled, additional ledger entries will be made for discounts in a separate Discount Account","Om aktiverad, extra bokföring poster kommer att skapas för rabatter på separat rabatt konto", +"If enabled, all files attached to this document will be attached to each email","Om aktiverad, kommer alla filer som bifogas detta dokument att bifogas till varje e-post meddelande", +"If enabled, do not update serial / batch values in the stock transactions on creation of auto Serial + / Batch Bundle. ","Om aktiverad, uppdatera inte serie nummer /parti värde i lager transaktioner vid skapande av automatiskt Serie Nummer + / Parti Paket. ", +"If enabled, ledger entries will be posted for change amount in POS transactions","Om aktiverad,bokföring poster kommer att bokföras för Växel Belopp i Kassa Transaktioner", +"If enabled, the consolidated invoices will have rounded total disabled",Om aktiverad kommer Konsoliderad Faktura att ha avrundad totalt inaktiverad, +"If enabled, the item rate won't adjust to the valuation rate during internal transfers, but accounting will still use the valuation rate.","Om aktiverad, kommer artikel pris inte att sättas till grund pris vid interna överföringar, men bokföring kommer fortfarande att använda grund pris.", +"If enabled, the system will create material requests even if the stock exists in the 'Raw Materials Warehouse'.","Om aktiverad, kommer system att skapa material begäran även om det finns på 'Råmaterial Lager'.", +"If enabled, the system will use the moving average valuation method to calculate the valuation rate for the batched items and will not consider the individual batch-wise incoming rate.","aaOm aktiverad, kommer system att använda Ma värdering sätt för att beräkna värdering för artikel partier och kommer inte att beakta individuell per parti pris.", +"If enabled, then system will only validate the pricing rule and not apply automatically. User has to manually set the discount percentage / margin / free items to validate the pricing rule","Om aktiverad, kommer system bara att validera prissättning regel och inte tillämpas automatiskt. Användare måste manuellt ange rabatt procent / marginal / gratis artikel för att validera prissättning regel", +"If mentioned, the system will allow only the users with this Role to create or modify any stock transaction earlier than the latest stock transaction for a specific item and warehouse. If set as blank, it allows all users to create/edit back-dated transactions.","Om angiven kommer system att tillåta Användare med denna roll att skapa eller ändra lager transaktioner tidigare än senaste lager transaktion för specifik artikel och lager. Om angiven som tomt, tillåter det alla Användare att skapa/redigera backdaterade transaktioner.", +"If not, you can Cancel / Submit this entry",Om inte kan man Annullera/Godkänna denna post, +"If rate is zero then item will be treated as ""Free Item""","Om pris är noll kommer artikel att behandlas som ""Gratis Artikel""", +"If the BOM results in Scrap material, the Scrap Warehouse needs to be selected.",Om Stycklista har Rest Material måste Rest Lager väljas., +"If the selected BOM has Operations mentioned in it, the system will fetch all Operations from BOM, these values can be changed.","Om vald Stycklista har angivna Åtgärder kommer system att hämta alla Åtgärder från Stycklista, dessa värden kan ändras.", +"If this checkbox is enabled, then the system won’t run the MRP for the available sub-assembly items.",Om vald kommer systemet inte att köra Material Resurs Planering för tillgängliga undermontering artiklar., +If this is undesirable please cancel the corresponding Payment Entry.,Om detta inte är önskvärt annullera motsvarande betalning post., +"If yes, then this warehouse will be used to store rejected materials","Om ja, kommer detta lager att användas för att lagra avvisat material", +"If you are maintaining stock of this Item in your Inventory, ERPNext will make a stock ledger entry for each transaction of this item.","Om man har denna artikel i Lager, kommer System att lagerbokföra varje transaktion av denna artikel.", +"If you need to reconcile particular transactions against each other, then please select accordingly. If not, all the transactions will be allocated in FIFO order.","Om man behöver stämma av specifika transaktioner mot varandra, välj därefter. Om inte, kommer alla transaktioner att tilldelas i FIFO ordning.", +"If you still want to proceed, please disable 'Skip Available Sub Assembly Items' checkbox.","Om du fortfarande vill fortsätta, avmarkera ""Hoppa över tillgängliga undermontering artiklar"".", +"If you still want to proceed, please enable {0}.","För att fortsätta, aktivera {0}.", +"If your CSV uses a different delimiter, add that character here, ensuring no spaces or additional characters are included.","Om CSV använder en annan avgränsare, lägg till det tecknet här, se till att inga mellanslag eller ytterligare tecken inkluderas.", +Ignore Account Closing Balance,Ignorera Bokföring Stängning Saldo, +Ignore Available Stock,Ignorera Tillgängligt Lager, +Ignore Closing Balance,Ignorera Stängning Saldo, +Ignore Default Payment Terms Template,Ignorera Standard Betalning Villkor Mall , +Ignore Empty Stock,Ignorera Tom Lager, +Ignore Exchange Rate Revaluation Journals,Ignorera Växelkurs Omvärdering Journaler , +Ignore Pricing Rule is enabled. Cannot apply coupon code.,Ignorera att Prissättning Regel är aktiverad. Det går inte att använda kupong kod., +Ignore System Generated Credit / Debit Notes,Ignorera System Skapade Kredit / Debet Notor, +Ignore Voucher Type filter and Select Vouchers Manually,Ignorera Verifikat Typ filter och Välj Verifikat Manuellt, +Impairment,Nedskrivningar, +Import File,Importera Fil, +Import File Errors and Warnings,Importera Fil Fel och Varningar, +Import Log Preview,Importera Logg Förhandsvisning, +Import Preview,Importera Förhandsvisning, +Import Progress,Import Framsteg, +Import Type,Import Typ, +Import Using CSV file,Importera med hjälp av CSV fil, +Import Warnings,Import Varningar, +Import from Google Sheets,Importera från Google Sheets, +"Importing {0} of {1}, {2}","Importerar {0} av {1}, {2}", +In House,Intern, +In Minutes,I Minuter, +In Party Currency,I Parti Valuta, +In Transit Transfer,I Transit Överföring, +In Transit Warehouse,I Transit Lager, +In mins,I Minuter, +"In row {0} of Appointment Booking Slots: ""To Time"" must be later than ""From Time"".","På rad {0} av Möte Bokning Tider: ""Till Tid"" måste vara senare än ""Från Tid"".", +"In the case of 'Use Multi-Level BOM' in a work order, if the user wishes to add sub-assembly costs to Finished Goods items without using a job card as well the scrap items, then this option needs to be enable.","I fall att 'Använd Flernivå Stycklista' används i arbetsorder, om användare vill lägga till undermontering kostnader till färdiga artiklar utan att använda jobbkort samt rest artiklar, måste detta alternativ vara aktiverad.", +"In this section, you can define Company-wide transaction-related defaults for this Item. Eg. Default Warehouse, Default Price List, Supplier, etc.","I detta sektion kan man definiera bolagsomfattande transaktion relaterade standard inställningar för denna artikel. T.ex. Standard Lager, Standard Prislista, Leverantör, osv.", +Inactive Status,Inaktiv Status, +Include Account Currency,Inkludera Konto Valuta, +Include Closed Orders,Inkludera Stängda Ordrar, +Include Default FB Assets,Inkludera Standard Finans Register Tillgångar, +Include Disabled,Inkludera Inaktiverad, +Include Expired Batches,Inkludera Utgångna Partier, +Include Safety Stock in Required Qty Calculation,Inkludera Säkerhet Lager i Begärd Kvantitet Beräkning, +Include Timesheets in Draft Status,Inkludera Tidrapporter i Utkast Status, +Include Zero Stock Items,Inkludera artiklar ej tillgängliga i lager, +Incoming Call Handling Schedule,Inkommande Samtalshantering Schema, +Incoming Call Settings,Inkommande Samtal Inställningar, +Incoming Rate (Costing),Inköp Pris (Kostnad), +Incorrect Balance Qty After Transaction,Felaktig Saldo Kvantitet Efter Transaktion, +Incorrect Batch Consumed,Felaktig Parti Förbrukad, +Incorrect Check in (group) Warehouse for Reorder,Felaktig vald (grupp) Lager för Ombeställning, +Incorrect Component Quantity,Felaktig Komponent Kvantitet, +Incorrect Invoice,Felaktig Faktura, +Incorrect Movement Purpose,Felaktig Förflytning Syfte, +Incorrect Payment Type,Felaktig Betalning Typ, +Incorrect Reference Document (Purchase Receipt Item),Felaktig Referens Dokument (Inköp Följesedel Artikel), +Incorrect Serial No Valuation,Felaktig Serie Nummer Värdering, +Incorrect Serial Number Consumed,Felaktig Serie Nummer Förbrukad, +Incorrect Stock Value Report,Felaktig Lager Värde Rapport, +Incorrect Type of Transaction,Felaktig Typ av Transaktion, +Increase In Asset Life(Months),Utökning av Tillgång Livslängd (Månader), +Indent,Rekvisition, +Indirect Expense,Indirekt Kostnad, +Individual GL Entry cannot be cancelled.,Enskild Bokföring Post kan inte avbokas., +Individual Stock Ledger Entry cannot be cancelled.,Enskild Lager Register Post kan inte avbokas., +Initialize Summary Table,Initiera Översikt Tabell, +Insert New Records,Infoga Nya Poster, +Inspection Rejected,Kontroll Avvisad, +Inspection Submission,Kontroll Godkännande, +Instruction,Instruktion, +Insufficient Capacity,Otillräcklig Kapacitet, +Insufficient Stock for Batch,Otillräcklig Lager för Parti, +Inter Transfer Reference,Intern Överföring Referens, +Interest and/or dunning fee,Ränta och/eller Påminnelseavgift, +Internal,Intern, +Internal Customer,Intern Kund, +Internal Customer for company {0} already exists,Intern Kund för Bolag {0} finns redan, +Internal Sale or Delivery Reference missing.,Intern Försäljning eller Leverans Referens saknas., +Internal Sales Reference Missing,Intern Försäljning Referens saknas, +Internal Supplier,Intern Leverantör, +Internal Supplier for company {0} already exists,Intern Leverantör för Bolag {0} finns redan, +Internal Transfer Reference Missing,Intern Överföring Referens saknas, +Internal Transfers,Interna Överföringar, +Internal transfers can only be done in company's default currency,Interna Överföringar kan endast göras i bolag standard valuta, +Invalid,Ogiltig, +Invalid Allocated Amount,Ogiltig Tilldelad Belopp, +Invalid Amount,Ogiltig Belopp, +Invalid Auto Repeat Date,Ogiltig Återkommande Datum, +Invalid Cost Center,Ogiltig Resultat Enhet, +Invalid Delivery Date,Ogiltig Leverans Datum, +Invalid Document,Ogiltig Dokument, +Invalid Document Type,Ogiltig Dokument Typ, +Invalid Formula,Ogiltig Formel, +Invalid Group By,Ogiltig Gruppera Efter, +Invalid Item Defaults,Ogiltig Artikel Standard, +Invalid Ledger Entries,Ogiltiga Register Poster, +Invalid Primary Role,Ogiltig Primär Roll, +Invalid Priority,Ogiltig Prioritet, +Invalid Process Loss Configuration,Ogiltig Process Förlust Konfiguration, +Invalid Purchase Invoice,Ogiltig Inköp Faktura, +Invalid Qty,Ogiltig Kvantitet, +Invalid Schedule,Ogiltig Schema, +Invalid Serial and Batch Bundle,Felaktig Serie och Parti Paket, +Invalid Warehouse,Ogiltig Lager, +Invalid result key. Response:,Ogiltig resultat nyckel. Svar:, +Invalid value {0} for {1} against account {2},Ogiltigt värde {0} för {1} mot konto {2}, +Inventory Dimension,Lager Dimension, +Inventory Dimension Negative Stock,Lager Dimension Negativ Lager, +Inventory Settings,Lager Inställningar, +Invoice Cancellation,Faktura Annullering, +Invoice Limit,Faktura Gräns, +Invoice Portion (%),Faktura Andel (%), +Invoice and Billing,Faktura & Fakturering, +Invoiced Qty,Fakturerad Kvantitet, +Invoices and Payments have been Fetched and Allocated,Fakturor och Betalningar är Hämtade och Tilldelade, +Invoicing Features,Fakturering Funktioner, +Is Adjustment Entry,Är Justering Post, +Is Alternative,Är Alternativ, +Is Cash or Non Trade Discount,Är Kontant eller Ej Försäljning Rabatt, +Is Composite Asset,Är Sammansatt Tillgång, +Is Corrective Job Card,Är Korrigerande Jobbkort, +Is Corrective Operation,Är Korrigerande Åtgärd, +Is Expandable,Är Utvidningsbar, +Is Finished Item,Är Färdig Artikel, +Is Fully Depreciated,Är Helt Avskriven, +Is Group Warehouse,Är Grupp Lager, +Is Old Subcontracting Flow,Är Gammal Underleverantör Flöde, +Is Outward,Är Utgående, +Is Period Closing Voucher Entry,Är Period Stängning Verifikat Post, +Is Rate Adjustment Entry (Debit Note),Är Pris Justering Post (Debet Nota), +Is Recursive,Är Rekursiv, +Is Rejected,Är Avvisad, +Is Rejected Warehouse,Är Avvisad Lager, +Is Scrap Item,Är Rest Artikel, +Is Short Year, Är Kort År, +Is Standard,Är Standard, +Is Stock Item,Är Lager Artikel, +Is System Generated,Är System Skapad, +Is Tax Withholding Account,Är Moms Avdrag Konto, +Is Template,Är Mall, +Issue Analytics,Ärende Analys, +Issue Summary,Ärende Översikt, +Issue a debit note with 0 qty against an existing Sales Invoice,Utfärda Debet Nota med 0 i Kvantitet mot befintlig Försäljning Faktura, +Issuing cannot be done to a location. Please enter employee to issue the Asset {0} to,Utfärdande kan inte göras till plats. Ange personal att utfärda Tillgång {0} , +It can take upto few hours for accurate stock values to be visible after merging items.,Det kan ta upp till några timmar för korrekta lagervärden att vara synliga efter sammanslagning av artiklar., +"It's not possible to distribute charges equally when total amount is zero, please set 'Distribute Charges Based On' as 'Quantity'","Det är inte möjligt att fördela avgifter lika när det totala beloppet är noll, vänligen ange ""Distribuera Avgifter Baserat På"" som ""Kvantitet""", +Item Code (Final Product),Artikel Kod (Färdig Artikel), +Item Group wise Discount,Rabatt per Artikel Grupp, +Item Price Settings,Artikel Pris Inställningar, +"Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, Batch, UOM, Qty, and Dates.","Artikel Pris visas flera gånger baserat på Prislista, Leverantör/Kund, Valuta, Artikel, Parti, Enhet, Kvantitet och Datum.", +Item Reference,Artikel Referens, +Item Warehouse based reposting has been enabled.,Artikel Lager baserad omläggning är aktiverad., +Item and Warehouse,Artikel och Lager, +Item is removed since no serial / batch no selected.,Artikel tas bort eftersom ingen serie nummer/parti nummer är vald., +Item qty can not be updated as raw materials are already processed.,Artikel kvantitet kan inte uppdateras eftersom råmaterial redan är bearbetad., +Item rate has been updated to zero as Allow Zero Valuation Rate is checked for item {0},Artikel pris har ändrats till noll eftersom Tillåt Noll Grund Pris är vald för artikel {0}, +Item valuation reposting in progress. Report might show incorrect item valuation.,Artikel värdering omläggning pågår. Rapport kan visa felaktig artikelvärde., +Item {0} cannot be added as a sub-assembly of itself,Artikel {0} kan inte läggas till som underenhet av sig själv, +Item {0} cannot be ordered more than {1} against Blanket Order {2}.,Artikel {0} kan inte skapas order för mer än {1} mot Blankoavtal Order {2}., +Item {0} does not exist.,Artikel {0} finns inte., +Item {0} entered multiple times.,Artikel {0} är angiven flera gånger., +Item {0} is already reserved/delivered against Sales Order {1}.,Artikel {0} är redan reserverad/levererad mot Försäljning Order {1}., +Item {0} must be a Non-Stock Item,Artikel {0} måste vara Ej Lager Artikel, +Item {0} not found in 'Raw Materials Supplied' table in {1} {2},"Artikel {0} hittades inte i ""Råmaterial Levererad"" tabell i {1} {2}", +Item {0} not found.,Artikel {0} hittades inte., +Item {} does not exist.,Artikel {} finns inte., +Items & Pricing,Artiklar & Priser, +Items Catalogue,Artikel Katalog, +Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}.,Artiklar kan inte uppdateras eftersom underleverantör order är skapad mot Inköp Order {0}., +Items rate has been updated to zero as Allow Zero Valuation Rate is checked for the following items: {0},Artikel Pris har ändrats till noll eftersom Tillåt Noll Grund Pris är vald för följande artiklar: {0}, +Items to Be Repost,Artikel som ska Läggas om, +Items to Order and Receive,Inköp Artiklar, +Items to Reserve,Artiklar att Reservera, +Items {0} do not exist in the Item master.,Artikel {0} saknas i Artikel Register., +Job Capacity,Arbetskapacitet, +Job Card Operation,Jobbkort Åtgärd, +Job Card Scheduled Time,Jobbkort Schemalagd Tid, +Job Card Scrap Item,Jobbkort Rest Artikel, +Job Card and Capacity Planning,Jobbkort & Kapacitet Planering, +Job Cards,Jobbkort , +Job Paused,Jobb Pausad, +Job Worker,Jobb Ansvarig, +Job Worker Address,Jobb Ansvarig Adress, +Job Worker Address Details,Jobb Ansvarig Adress Detaljer, +Job Worker Contact,Jobb Ansvarig Kontakt, +Job Worker Delivery Note,Jobb Ansvarig Följesedel, +Job Worker Name,Jobb Ansvarig Namn, +Job Worker Warehouse,Jobb Ansvarig Lager, +Job: {0} has been triggered for processing failed transactions,Jobb: {0} är utlöst för bearbetning av misslyckade transaktioner, +Joining,Anställning, +Journal Entries,Journal Poster, +Journal Entry for Asset scrapping cannot be cancelled. Please restore the Asset.,Journal Post för Tillgång avskrivning kan inte annulleras. Vänligen återställ Tillgång., +Journal Entry type should be set as Depreciation Entry for asset depreciation,Journal Post Typ ska anges som Avskrivning Post för tillgång avskrivning, +Journal entries have been created,Journal Poster är skapade, +Journals,Journaler, +Key,Nyckel, +Kindly cancel the Manufacturing Entries first against the work order {0}.,Vänligen annullera Produktion Poster först mot Arbetsorder {0}., +"Last Name, Email or Phone/Mobile of the user are mandatory to continue.","Efternamn, E-post eller Telefon/Mobil erfodras för användare att fortsätta.", +Lead -> Prospect,Potentiell Kund -> Prospekt, +Lead Conversion Time,Potentiell Kund Samtal Tid, +Lead Owner cannot be same as the Lead Email Address,Potentiell Kund Ansvarig kan inte vara samma som Potentiell Kund E-post Adress, +Lead {0} has been added to prospect {1}.,Potentiell Kund {0} är lagd till Prospekt {1}., +Leaderboard,Topplista, +Leads,Potentiella Kunder, +Learn Accounting,Lär dig om Bokföring, +Learn Inventory Management,Lär dig om Lager Hantering, +Learn Manufacturing,Lär dig om Produktion, +Learn Procurement,Lär dig om Inköp, +Learn Project Management,Lär dig om Projekt Hantering, +Learn Sales Management,Lär dig om Försäljning, +"Learn about Common Party","Lär dig om Gemensam Parti", +"Leave blank for home. +This is relative to site URL, for example ""about"" will redirect to ""https://yoursitename.com/about""","Lämna tom för Hem. Detta är relativt till webbadress, till exempel 'Om' kommer att omdirigera till 'https://yoursitename.com/about'", +Ledger Health,Register Status, +Ledger Health Monitor,Register Status Övervakning, +Ledger Health Monitor Company,Register Status Övervakning Bolag, +Ledger Merge,Bokgöring Register Sammanslagning, +Ledger Merge Accounts,Register Sammanslagning Konton, +Ledgers,Register, +Left Child,Vänster Underordnad, +Legend,Förklaring, +Length,Längd, +Length (cm),Längd (cm), +Less than 12 months.,Kortare än tolv månader., +Level (BOM),Nivå (Stycklista), +Limit timeslot for Stock Reposting,Begränsa tid för Lager Omläggning, +Limits don't apply on,Gräns gäller inte, +Link a new bank account,Länka ny Bank Konto, +Link with Customer,Länka med Kund, +Link with Supplier,Länka med Leverantör, +Linked with submitted documents,Länkad med godkända dokument, +Linking Failed,Länkning Misslyckad, +Linking to Customer Failed. Please try again.,Länkning med Kund Misslyckades. Var god försök igen., +Linking to Supplier Failed. Please try again.,Länkning med Leverantör Misslyckades. Var god försök igen., +Links,Länkar, +Loading import file...,Laddar import fil..., +Locked,Låst, +Log Entries,Logg Poster, +Log the selling and buying rate of an Item,Logga försäljning och inköp pris för Artikel, +Lost Quotations,Förlorade Försäljning Offerter, +Lost Quotations %,Förlorade Försäljning Offerter (%), +Lost Reasons are required in case opportunity is Lost.,Förlorad Anledning erfordras om Möjlighet är Förlorad., +Lost Value,Förlorad Värde, +Lost Value %,Förlorad Värde %, +Machine Type,Maskin Typ, +Main Cost Center,Standard Resultat Enhet, +Main Cost Center {0} cannot be entered in the child table,Huvud Resultat Enhet {0} kan inte anges i underordnad tabell, +Maintain Asset,Underhåll Tillgång, +Maintenance Details,Service Detaljer, +Make ,Märke , +Make Asset Movement,Skapa Tillgång Förflyttning, +Make Quotation,Skapa Offert, +Make Return Entry,Skapa Retur Post, +Make Serial No / Batch from Work Order,Skapa Serie / Parti Nummer från Arbetsorder, +Make {0} Variant,Skapa {0} Variant, +Make {0} Variants,Skapa {0} Varianter, +Making Journal Entries against advance accounts: {0} is not recommended. These Journals won't be available for Reconciliation.,Skapa Journal Poster mot förskott konton: {0} rekommenderas inte. Dessa journaler kommer inte att vara tillgängliga för avstämning., +Manage,Hantera, +Mandatory Accounting Dimension,Erfodrad Bokföring Dimension, +Mandatory Depends On,Erfordrad Beroende Av, +Mandatory Field,Erfodrad Fält, +Mandatory Section,Erfodrad Sektion, +Manual Inspection,Manuell Kontroll, +Manufacture Sub-assembly in Operation,Producera Underenhet i Åtgärd, +Manufacturing Type,Produktion Typ, +Mapping Purchase Receipt ...,Mappar Inköp Följesedel..., +Mapping Subcontracting Order ...,Mappar Underleverantör Order ..., +Mapping {0} ...,Mappar {0} ..., +Margin View,Marginal Vy, +Mark As Closed,Ange som Stängd , +Material Returned from WIP,Material Retur från Arbete Pågår, +Material Transfer (In Transit),Material Överföring (I Transit), +Materials are already received against the {0} {1},Material mottagen mot {0} {1}, +Materials needs to be transferred to the work in progress warehouse for the job card {0},Material måste överföras till Arbete Pågår Lager för Jobbkort {0}, +Max Qty (As Per Stock UOM),Maximum Kvantitet (per Lager Enhet), +Maximum Net Rate,Maximum Netto Pris, +Maximum Payment Amount,Maximum Betalning Belopp, +Maximum Value,Maximum Värde, +Maximum quantity scanned for item {0}.,Maximum kvantitet skannad för artikel {0}., +Meeting,Möte, +Mention if non-standard Receivable account,Ange om ej Standard Fordring Konto, +Merge Invoices Based On,Slå Samman Faktura Baserad På, +Merge Progress,Sammanslagning Framsteg, +Merge Similar Account Heads,Slå Samman Liknande Konto Poster, +Merge taxes from multiple documents,Slå Samman Moms från flera dokument, +Merged,Sammanslagen, +"Merging is only possible if following properties are same in both records. Is Group, Root Type, Company and Account Currency","Sammanslagning är endast möjlig om följande egenskaper är lika i båda poster. Är Grupp, Konto Klass, Bolag och Konto Valuta", +Merging {0} of {1},Slår Samman {0} av {1}, +Min Qty (As Per Stock UOM),Minimum Kvantitet (per Lager Enhet), +Min Qty should be greater than Recurse Over Qty,Minimum Kvantitet ska vara högre än Rekurs över kvantitet, +Minimum Net Rate,Minimum Netto Pris, +Minimum Payment Amount,Minimum Betalning Belopp, +Minimum Value,Mimimum Värde, +Mismatch,Felavstämd, +Missing,Saknas, +Missing Asset,Tillgång Saknas, +Missing Cost Center,Resultat Enhet Saknas, +Missing Finance Book,Finans Register Saknas, +Missing Finished Good,Färdig Artikel Saknas, +Missing Formula,Formel Saknas, +Missing Items,Artiklar Saknas, +Missing Payments App,Betalning App Saknas, +Missing Serial No Bundle,Serie Nummer Paket Saknas, +Missing value,Värde Saknas, +Modified By,Modifierad Av, +Modified On,Modifierad, +Module Settings,Modul Inställningar, +Monitor for Last 'X' days,"Övervakning för senaste ""X"" dagar", +Move Stock,Flytta Lager, +Move to Cart,Flytta till Kundkorg, +Movement,Förflyttning, +Moving up in tree ...,Flytta upp i träd..., +Multi-level BOM Creator,Fler Nivå Stycklista Generator, +Multiple Loyalty Programs found for Customer {}. Please select manually.,Flera Lojalitet Program hittades för Kund {}. Välj manuellt., +Multiple Warehouse Accounts,Flera Lager Konton, +Multiple items cannot be marked as finished item,Flera artiklar kan inte väljas som färdiga artiklar, +Must be a publicly accessible Google Sheets URL and adding Bank Account column is necessary for importing via Google Sheets,Måste vara publik tillgänglig URL för Google Sheets och det är nödvändigt att lägga till Bank Konto Kolumn att importera via Google Sheets, +Named Place,Namngiven Plats, +Naming Series and Price Defaults,Nummer Serie & Pris Standard, +Net total calculation precision loss,Netto Total Beräkning Precision Förlust, +New Balance In Account Currency,Ny Saldo i Konto Valuta, +New Event,Ny Händelse, +New Note,Ny Anteckning, +New Task,Ny Uppgift, +New Version,Ny Version, +Newsletter,Nyhetsbrev, +No Answer,Ingen Svar, +No Customers found with selected options.,Inga Kunder hittades med valda alternativ., +No Items selected for transfer.,Inga Artiklar har valts för överföring., +No Matching Bank Transactions Found,Inga matchande banktransaktioner hittades, +No Notes,Inga Anteckningar, +No Outstanding Invoices found for this party,Inga Utestående Fakturor hittades för denna parti, +No POS Profile found. Please create a New POS Profile first,Ingen Kassa Profil hittad. Skapa ny Kassa Profil, +No Records for these settings.,Inga Poster för dessa inställningar., +No Serial / Batches are available for return,Inga Serie Nummer/Partier är tillgängliga för retur, +No Stock Available Currently,Ingen Lager Tillgänglig för närvarande, +No Summary,Ingen Översikt, +No Tax Withholding data found for the current posting date.,Ingen Moms Avdrag data hittades för aktuell bokföring datum., +No Terms,Inga Villkor, +No Unreconciled Invoices and Payments found for this party and account,Inga Oavstämda Fakturor och Betalningar hittades för denna parti och konto, +No Unreconciled Payments found for this party,Inga Oavstämda Betalningar hittades för denna parti, +No Work Orders were created,Inga Arbetsordrar skapades, +No additional fields available,Inga extra fält tillgängliga, +No billing email found for customer: {0},Ingen faktura e-post hittades för kund: {0}, +No data found. Seems like you uploaded a blank file,Ingen data hittades. Det verkar som om tom fil laddats upp, +No employee was scheduled for call popup,Ingen personal var schemalagd för oväntad samtal, +No failed logs,Inga Misslyckade Logg, +No item available for transfer.,Ingen artikel tillgänglig för överföring., +No items are available in sales orders {0} for production,Inga artiklar är tillgängliga i Försäljning Order {0} för produktion, +No items are available in the sales order {0} for production,Inga artiklar är tillgängliga i Försäljning Order {0} för produktion, +No items in cart,Antal Artiklar i Kundkorg, +No matches occurred via auto reconciliation,Inga avstämningar uppstod via automatisk avstämning, +No more children on Left,Inga fler underordnade till Vänster, +No more children on Right,Inga fler underordnade till Höger, +No of Docs,Antal Dokument , +No of Employees,Personal Antal, +No of Months (Expense),Antal Månader, +No of Months (Revenue),Antal Månader, +No open event,Inga öppna Händelse, +No open task,Inga öppna Uppgifter, +No outstanding {0} found for the {1} {2} which qualify the filters you have specified.,Inga utestående {0} hittades för {1} {2} som uppfyller angiven filter., +No primary email found for customer: {0},Ingen primär e-post adress hittades för kund: {0}, +No records found in Allocation table,Inga poster hittades i Tilldelning tabell, +No records found in the Invoices table,Inga poster hittades i Faktura Tabell, +No records found in the Payments table,Inga poster hittades i Betalning Tabell, +No stock transactions can be created or modified before this date.,Inga lager transaktioner kan skapas eller ändras före detta datum., +No {0} Accounts found for this company.,Inga {0} konto hittades för detta bolag., +No.,Nr., +No. of Employees,Personal Antal, +No. of parallel job cards which can be allowed on this workstation. Example: 2 would mean this workstation can process production for two Work Orders at a time.,Antal samtidiga Jobbkort som kan tillåtas på denna arbetsstation. Exempel: 2 skulle innebära att denna arbetsstation kan hantera två Arbetsordrar åt gången., +Note: Automatic log deletion only applies to logs of type Update Cost,Obs: Automatisk logg radering gäller endast loggar av typ Uppdatera Kostnad, +"Note: To merge the items, create a separate Stock Reconciliation for the old item {0}",Obs: För att slå samman artiklar skapar separat lager avstämning för gamla artikel {0}, +Notes HTML,Anteckningar HTML, +Notification,Aviseringar, +Notification Settings,Avisering Inställningar, +Notify Reposting Error to Role,Avisera omläggning fel till roll, +Number of Days,Antal Dagar, +Numeric,Numerisk, +Numeric Inspection,Numerisk Kontroll, +Off,Av, +Offsetting Account,Avräkning Konto, +Offsetting for Accounting Dimension,Avräkning för Bokföring Dimension, +On Paid Amount,På Betald Belopp, +On This Date,Datum, +On Track,På Bana, +On enabling this cancellation entries will be posted on the actual cancellation date and reports will consider cancelled entries as well,När du aktiverar denna annullering kommer poster att publiceras på faktisk annullering datum och rapporter kommer också att inkludera annullerade poster, +"On expanding a row in the Items to Manufacture table, you'll see an option to 'Include Exploded Items'. Ticking this includes raw materials of the sub-assembly items in the production process.","Vid utökning av rad i Artiklar att Producera Tabell,kommer du att se alternativ ""Inkludera Utvidgade Artiklar"". Genom att väljadetta ingår råvaror från delkomponenter i produktion process.", +"On submission of the stock transaction, system will auto create the Serial and Batch Bundle based on the Serial No / Batch fields.",Vid godkännade av lager transaktion kommer system att automatiskt skapa Serie och Parti Paket baserat på Serienummer / Parti fält., +Once the Work Order is Closed. It can't be resumed.,När Arbetsorder är Stängd kan den inte återupptas., +Only 'Payment Entries' made against this advance account are supported.,"Endast ""Betalning Poster"" som skapas mot detta förskott konto stöds.", +Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload,Endast CSV och Excel filer kan användas för data import. Kontrollera filformat du försöker ladda upp, +Only Deduct Tax On Excess Amount ,Endast Dra av Skatt på Överskjutande Belopp, +Only Include Allocated Payments,Endast Inkludera allokerade betalningar, +Only Parent can be of type {0},Endast Överordnad kan vara av typ {0}, +Only existing assets,Endast Befintliga Tillgångar, +"Only one Subcontracting Order can be created against a Purchase Order, cancel the existing Subcontracting Order to create a new one.","Endast en Underleverantör Order kan skapas mot en Inköp Order, annullera befintlig Underleverantör Order för att skapa ny.", +Only one {0} entry can be created against the Work Order {1},Endast en {0} post kan skapas mot Arbetsorder {1}, +"Only values between [0,1) are allowed. Like {0.00, 0.04, 0.09, ...} +Ex: If allowance is set at 0.07, accounts that have balance of 0.07 in either of the currencies will be considered as zero balance account","Endast värden mellan [0,1) är tillåtna, t.ex.{0.00, 0.04, 0.09, ...} +Exempel: Om tillåtelse är angiven till 0,07, kommer konton som har saldo på 0,07 i någon av valutorna att betraktas som noll saldo konto.", +Only {0} are supported,Endast {0} stöds, +Open Activities HTML,Öppna Aktiviteter HTML, +Open Call Log,Öppna Samtal Logg, +Open Event,Öppna Händelse, +Open Events,Öppna Händelser, +Open Sales Orders,Öppna Försäljning Ordrar, +Open Task,Öppna Uppgift, +Open Tasks,Öppna Uppgifter, +Open Work Order {0},Öppna Arbetsorder {0}, +Opening & Closing,Öppning & Stängning, +Opening Accumulated Depreciation must be less than or equal to {0},Öppning Ackumulerad Avskrivning måste vara mindre än eller lika med {0}, +Opening Entry can not be created after Period Closing Voucher is created.,Öppning Post kan inte skapas efter att Period Stängning Verifikat är skapad., +Opening Number of Booked Depreciations,Öppning Nummer för Bokförda Avskrivningar, +Opening Purchase Invoices have been created.,Öppning Inköp Fakturor är skapade., +Opening Sales Invoices have been created.,Öppning Försäljning Fakturor är skapade., +Operating Cost Per BOM Quantity,Drift Kostnad per Stycklista Kvantitet, +Operation time does not depend on quantity to produce,Åtgärd Tid beror inte på kvantitet som ska produceras, +Opportunity Amount (Company Currency),Möjlighet Belopp (Bolag Valuta), +Opportunity Owner,Möjlighet Ansvarig, +Opportunity Source,Möjlighet Källa, +Opportunity Summary by Sales Stage,Möjlighet Översikt efter Försäljning Stadium, +Opportunity Summary by Sales Stage ,Möjlighet Översikt efter Försäljning Stadium, +Opportunity Value,Möjlighet Värde, +Other Info,Övrig Information, +Out of stock,Ej på Lager, +Over Picking Allowance,Över Plock Tillåtelse, +Over Receipt,Över Följesedel, +Over Receipt/Delivery of {0} {1} ignored for item {2} because you have {3} role.,Över Följesedel/Leverans av {0} {1} ignoreras för artikel {2} eftersom du har {3} roll., +Over Transfer Allowance,Över Överföring Tillåtelse, +Overbilling of {0} {1} ignored for item {2} because you have {3} role.,Överfakturering av {0} {1} ignoreras för artikel {2} eftersom du har {3} roll., +Overbilling of {} ignored because you have {} role.,Överfakturering av {} ignoreras eftersom du har {} roll., +Overdue Payment,Förfallna Fakturor, +Overdue Payments,Förfallna Fakturor, +Overdue Tasks,Försenade Uppgifter, +Overview,Recension , +PDF Name,PDF Namn, +POS Closing Failed,Kassa Stängning Misslyckad, +POS Closing failed while running in a background process. You can resolve the {0} and retry the process again.,Kassa Stängning misslyckades medan den kördes i bakgrund process. Du kan lösa {0} och försöka igen., +POS Invoice is already consolidated,Kassa Faktura är redan konsoliderad, +POS Invoice is not submitted,Kassa Faktura är inte godkänd, +POS Invoice should have the field {0} checked.,Kassa Faktura ska ha {} fält vald., +POS Invoices will be consolidated in a background process,Kassa Fakturor kommer att konsolideras i bakgrund process, +POS Invoices will be unconsolidated in a background process,Kassa Fakturor kommer att okonsolideras i bakgrund process, +POS Profile doesn't match {},Kassa Profil matchar inte {}, +POS Profile {} contains Mode of Payment {}. Please remove them to disable this mode.,Kassa Profil {} innehåller Betalning Sätt {}. Ta bort Betalning Sätt för att inaktivera detta läge., +POS Search Fields,Kassa Sök Fält, +POS Setting,Kassa Inställningar, +Package No(s) already in use. Try from Package No {0},Paket Nummer används redan. Prova från Paket Nummer {0} , +Packaging Slip From Delivery Note,Packsedel från Försäljning Följesedel, +Packed Items cannot be transferred internally,Packade artiklar kan inte överföras internt, +Packed Qty,Förpackad Kvantitet, +Page Break After Each SoA,Sidbrytning efter varje SoA, +Paid Amount After Tax,Betald Belopp efter Moms, +Paid Amount After Tax (Company Currency),Betald Belopp efter Moms (Bolag Valuta), +Paid From Account Type,Betald från Konto Typ, +Paid To Account Type,Betald till Konto Typ, +Pallets,Pall, +Parameter Group,Parameter Grupp, +Parameter Group Name,Parameter Grupp Namn, +Parcel Template,Leverans Paket Mall, +Parcel Template Name,Leverans Paket Mall Namn, +Parcel weight cannot be 0,Leverans Paket Vikt får inte vara 0, +Parcels,Paket, +Parent Account Missing,Överordnad Konto Saknas, +Parent Document,Överordnad Dokument, +Parent Item {0} must not be a Fixed Asset,Överordnad Artikel {0} får inte vara Tillgång, +Parent Row No,Överordnad Rad Nummer, +Parent Task {0} is not a Template Task,Överordnad Uppgift {0} är inte Mall Uppgift, +Partial Material Transferred,Delvis Material Överförd, +Partial Stock Reservation,Delvis Lager Reservation, +Partial Success,Delvis Klar, +"Partial stock can be reserved. For example, If you have a Sales Order of 100 units and the Available Stock is 90 units then a Stock Reservation Entry will be created for 90 units. ","Delvis Lager kan reserveras. Till exempel, om du har en försäljning order på 100 enheter och det tillgängliga lagret är 90 enheter kommer lager reservation post att skapas för 90", +Partially Delivered,Delvis Levererad, +Partially Reconciled,Delvis Avstämd, +Partially Reserved,Delvis Reserverad, +Partly Paid,Delvis Betald, +Partly Paid and Discounted,Delvis Betald och Rabatterad, +Partnership,Partner, +Party Account No. (Bank Statement),Parti Konto Nummer (Kontoutdrag), +Party Account {0} currency ({1}) and document currency ({2}) should be same,Parti Konto {0} valuta ({1}) och dokument valuta ({2}) ska vara samma, +Party IBAN (Bank Statement),Parti IBAN (Kontoutdrag), +Party Item Code,Parti Artikel Kod, +Party Link,Parti Länk, +Party Name/Account Holder (Bank Statement),Parti Namn/Kontoinnehavare (Kontoutdrag), +Party Specific Item,Parti Specifik Artikel, +Party Type and Party is required for Receivable / Payable account {0},Parti Typ och Parti erfodras för Fordring / Skuld konto {0}, +Party can only be one of {0},Parti kan endast vara en av {0}, +Passport Details,Passport Detaljer, +Pause Job,Pausa Jobb, +Paused,Pausad, +Payment Amount (Company Currency),Faktura Belopp (Bolag Valuta), +"Payment Entry {0} is linked against Order {1}, check if it should be pulled as advance in this invoice.","Betalning Post {0} är länkad till Order {1}, kontrollera om den ska hämtas som förskott på denna faktura.", +Payment Ledger,Betalning Register, +Payment Ledger Balance,Betalning Register Saldo, +Payment Ledger Entry,Betalning Register Post, +Payment Limit,Betalning Gräns, +Payment Reconciliation Allocation,Betalning Avstämning Tilldelning , +Payment Reconciliation Job: {0} is running for this party. Can't reconcile now.,Betalning Avstämning Jobb: {0} körs för denna parti. Kan inte avstämas nu., +Payment Reconciliations,Betalning Avstämningar, +Payment Request Outstanding,Betalning Begäran Utestående Belopp, +Payment Request created from Sales Order or Purchase Order will be in Draft status. When disabled document will be in unsaved state.,Betalning Begäran som skapas från Försäljning Order eller Inköp Order kommer att få Utkast status. När inaktiverad kommer dokument att vara i osparat tillstånd., +Payment Request is already created,Betalning Begäran är redan skapad, +Payment Request took too long to respond. Please try requesting for payment again.,Betalning Begäran tog för lång tid att svara. Försök att begära betalning igen., +Payment Requests cannot be created against: {0},Betalning Begäran kan inte skapas mot: {0}, +Payment Term Outstanding,Betalningsvillkor Utestående Belopp, +Payment Terms Status for Sales Order,Betalning Villkor Status för Försäljning Order, +Payment Terms from orders will be fetched into the invoices as is,Betalning Villkor från Order hämtas till Faktura som den är, +Payment Unlink Error,Betalning Bortkoppling Fel, +Payment of {0} received successfully.,Betalning på {0} mottagen., +Payment of {0} received successfully. Waiting for other requests to complete...,Betalning på {0} mottagen. Väntar på att andra begäran ska slutföras..., +Payment request failed,Betalning Begäran Misslyckades, +Payment term {0} not used in {1},Betalning Villkor {0} används inte i {1}, +Pending processing,Väntar på bearbetning, +Per Received,Per Mottagen, +Percentage (%),Procent (%), +Percentage you are allowed to order beyond the Blanket Order quantity.,Procentandel tillåten för order utöver Blankoavtal Order Kvantitet., +Percentage you are allowed to sell beyond the Blanket Order quantity.,Procentandel tillåten för försäljning utöver Blankoavtal Order Kvantitet., +Period Closed,Period Stängd, +Period Closing Entry For Current Period,Period Låsning Post för Aktuell Period, +Period Closing Settings,Period Låsning Inställningar, +Period Details,Period Detaljer, +Period To Date,Period Till Datum, +Period_from_date,Period Från Datum, +Phone Ext.,Telefon Anknytning, +Pick List Incomplete,Plocklista Ofullständig, +Pick Manually,Plocka Manuellt, +Pick Serial / Batch Based On,Välj Serie / Parti Baserad På, +Pick Serial / Batch No,Välj Serie / Parti Nummer, +Picked Qty (in Stock UOM),Plockad Kvantitet (Lager Enhet), +Pickup,Hämtning, +Pickup Contact Person,Hämtning Adress Kontakt Person, +Pickup Date,Hämtning Datum, +Pickup Date cannot be before this day,Hämtning Datum kan inte infalla före denna dag, +Pickup From,Hämtning Från, +Pickup To time should be greater than Pickup From time,Hämtning Till Tid ska vara senare än Hämtning Från Tid, +Pickup Type,Hämtning Typ, +Pickup from,Hämtning Från, +Pickup to,Hämtning Till, +Pipeline By,Tratt Efter, +Plaid Link Failed,Plaid Länk Misslyckades, +Plaid Link Refresh Required,Plaid Länk Uppdatering erfordras, +Plaid Link Updated,Plaid Länk Uppdaterad, +Plan to Request Qty,Planerad Kvantitet, +Plant Dashboard,Fabrik Översikt Panel, +Plant Floor,Produktion Yta, +Please Set Priority,Ange Prioritet, +Please Specify Account,Specificera Konto, +Please add 'Supplier' role to user {0}.,"Lägg till Roll ""Leverantör"" till användare {0}.", +Please add Request for Quotation to the sidebar in Portal Settings.,Lägg till Offert Förfråga i sidofält i Portal Inställningar., +Please add Root Account for - {0},Lägg till Överordnad Konto för - {0}, +Please add atleast one Serial No / Batch No,Lägg till minst en Serie Nr / Parti Nr, +Please add the Bank Account column,Lägg till Bank Konto kolumn, +Please add the account to root level Company - {0},Lägg till Konto till Överordnad Bolag - {0}, +Please add {1} role to user {0}.,Lägg till roll {1} till användare {0}., +Please adjust the qty or edit {0} to proceed.,Justera kvantitet eller redigera {0} för att fortsätta., +Please attach CSV file,Bifoga CSV Fil, +Please cancel and amend the Payment Entry,Annullera och ändra Betalning Post, +Please cancel payment entry manually first,Annullera Betalning Post manuellt, +Please cancel related transaction.,Annullera relaterad transaktion., +Please check Process Deferred Accounting {0} and submit manually after resolving errors.,Välj Bearbeta Uppskjuten Bokföring {0} och godkänn manuellt efter att ha löst fel., +Please check either with operations or FG Based Operating Cost.,Välj antingen Med Åtgärder eller Färdig Artikel Baserad Åtgärd Kostnad., +Please check the error message and take necessary actions to fix the error and then restart the reposting again.,Kontrollera felmeddelande och vidta nödvändiga åtgärder för att åtgärda fel och starta sedan ompostning igen., +Please check your email to confirm the appointment,Kontrollera din E-post för att bekräfta Möte, +Please contact any of the following users to extend the credit limits for {0}: {1},Kontakta någon av följande användare för att utöka kredit gränser för {0}: {1}, +Please contact any of the following users to {} this transaction.,Kontakta någon av följande användare för att {} denna transaktion., +Please contact your administrator to extend the credit limits for {0}.,Kontakta administratör för att utöka kredit gränser för {0}., +Please create Landed Cost Vouchers against Invoices that have 'Update Stock' enabled.,"Skapa Landad Kostnad Verifikat mot fakturor som har ""Uppdatera Lager"" aktiverad.", +Please create a new Accounting Dimension if required.,Skapa Bokföring Dimension vid behov., +Please create purchase from internal sale or delivery document itself,Skapa Inköp från intern Försäljning eller Följesedel, +"Please delete Product Bundle {0}, before merging {1} into {2}",Ta bort Artikel Paket {0} innan sammanslagning av {1} med {2}, +Please do not book expense of multiple assets against one single Asset.,Bokför inte kostnader för flera Tillgångar mot enskild Tillgång., +Please enable Use Old Serial / Batch Fields to make_bundle,Aktivera Använd gamla Serie / Parti Fält för att skapa paket, +Please enable only if the understand the effects of enabling this.,Aktivera endast om du förstår effekterna av att aktivera detta., +Please enable {0} in the {1}.,Aktivera {0} i {1}., +Please enable {} in {} to allow same item in multiple rows,Aktivera {} i {} för att tillåta samma Artikel i flera rader, +Please ensure that the {0} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account.,Kontrollera att {0} konto är Balans Rapport Konto. Ändra Överordnad Konto till Balans Rapport Konto eller välj annat konto., +Please ensure that the {0} account {1} is a Payable account. You can change the account type to Payable or select a different account.,Kontrollera att {0} konto {1} är Skuld Konto. Ändra Konto Typ till Skuld Konto Typ eller välj ett annat konto., +Please ensure {} account is a Balance Sheet account.,Kontrollera att {} konto är Balans Rapport konto., +Please ensure {} account {} is a Receivable account.,Kontrollera att {} konto {} är fordring konto., +Please enter Root Type for account- {0},Ange Konto Klass för konto {0}, +Please enter Serial Nos,Ange Serie Nummer, +Please enter Shipment Parcel information,Ange Leverans Paket information, +Please enter Stock Items consumed during the Repair.,Ange Artiklar förbrukade under reparation., +Please enter mobile number first.,Ange Mobil Nummer, +Please enter quantity for item {0},Ange Kvantitet för artikel {0}, +Please enter serial nos,Ange Serie Nummer, +"Please first set Last Name, Email and Phone for the user","Ange Efternamn, E-post och Telefon för användare", +Please fix overlapping time slots for {0},Åtgärda överlappande tider för {0}, +Please fix overlapping time slots for {0}.,Åtgärda överlappande tider för {0}, +Please import accounts against parent company or enable {} in company master.,Importera konton mot moderbolag eller aktivera {} i bolag inställningar., +"Please keep one Applicable Charges, when 'Distribute Charges Based On' is 'Distribute Manually'. For more charges, please create another Landed Cost Voucher.","Behåll en Tillämplig Avgift när ""Distribuera avgifter Baserat på"" är ""Distribuera Manuellt"". För fler avgifter, skapa annan Landad Kostnad Verifikat", +Please make sure the file you are using has 'Parent Account' column present in the header.,"Kontrollera att fil har kolumn ""Överordnad Konto"" i rubrik.", +Please mention 'Weight UOM' along with Weight.,"Ange ""Vikt Enhet"" tillsammans med Vikt.", +Please mention the Current and New BOM for replacement.,Ange Aktuell och Ny Stycklista för ersättning., +Please rectify and try again.,Rätta till och försök igen., +Please refresh or reset the Plaid linking of the Bank {}.,Uppdatera eller återställ Plaid Länk för Bank {}., +Please save before proceeding.,Spara innan du fortsätter., +Please select Bank Account,Välj Bank Konto, +Please select Finished Good Item for Service Item {0},Välj Färdig Artikel för Service Artikel {0}, +Please select Serial/Batch Nos to reserve or change Reservation Based On to Qty.,Välj Serie / Parti Nummer att reservera eller ändra Reservation Baserad På Kvantitet., +Please select Subcontracting Order instead of Purchase Order {0},Välj Underleverantör Order istället för Inköp Order {0}, +Please select Unrealized Profit / Loss account or add default Unrealized Profit / Loss account account for company {0},Välj Orealiserad Resultat Konto eller ange standard konto för Orealiserad Resultat Konto för Bolag {0}, +Please select a Subcontracting Purchase Order.,Välj Underleverantörer Inköp Order, +Please select a Warehouse,Välj Lager, +Please select a Work Order first.,Välj Arbetsorder, +Please select a country,Välj Land, +Please select a customer for fetching payments.,Välj Kund för att hämta betalningar., +Please select a date,Välj Datum, +Please select a date and time,Välj Tid och Datum, +Please select a row to create a Reposting Entry,Välj rad att skapa Ompostering Post, +Please select a supplier for fetching payments.,Välj Leverantör för att hämta betalningar., +Please select a valid Purchase Order that has Service Items.,Välj giltig Inköp Order med Service Artiklar., +Please select a valid Purchase Order that is configured for Subcontracting.,Välj giltig Inköp Order som är konfigurerad för Underleverantör., +Please select an item code before setting the warehouse.,Välj Artikel Kod innan du anger Lager., +Please select either the Item or Warehouse or Warehouse Type filter to generate the report.,"Välj antingen Artikel,Lager eller Lager Typ filter att skapa rapport.", +Please select items,Välj Artiklar, +Please select items to reserve.,Välj Artiklar att reservera, +Please select items to unreserve.,Välj Artiklar att reservera, +Please select only one row to create a Reposting Entry,Vänligen välj endast en rad för att skapa Ompostering Post, +Please select rows to create Reposting Entries,Välj rader för att skapa Ompostering Poster, +Please select the required filters,Välj de filter som krävs, +Please select valid document type.,Välj giltig dokument typ., +Please set Account,Ange Konto, +Please set Accounting Dimension {} in {},Ange Bokföring Dimension {} i {}, +Please set Email/Phone for the contact,Ange E-post/Telefon för Kontakt, +Please set Fiscal Code for the customer '%s',"Ange Org.Nr. för Kund ""%s""", +Please set Fiscal Code for the public administration '%s',"Ange Org.Nr. för Offentlig Förvaltning ""%s""", +Please set Fixed Asset Account in {} against {}.,Ange Tillgång Konto i {} mot {}., +Please set Opening Number of Booked Depreciations,Ange Öppning Nummer för Bokförda Avskrivningar, +Please set Parent Row No for item {0},Ange Överordnad Rad Nummer för artikel {0}, +Please set Root Type,Ange Konto Klass, +Please set Tax ID for the customer '%s',Ange Org.Nr. for Kund '%s', +Please set VAT Accounts in {0},Ange Moms Konton i {0}, +"Please set Vat Accounts for Company: ""{0}"" in UAE VAT Settings","Ange Moms Konton för Bolag: ""{0}"" i moms inställningarna i Förenade Arabemirater", +Please set a Cost Center for the Asset or set an Asset Depreciation Cost Center for the Company {},Ange Resultat Enhet för Tillgång eller ange Resultat Enhet för Tillgång Avskrivningar för Bolag {}, +Please set a default Holiday List for Company {0},Ange standard Helg Lista för Bolag {0}, +Please set an Address on the Company '%s',Ange adress för Bolag '%s', +Please set an Expense Account in the Items table,Ange Kostnad konto i Artikel tabell, +Please set default Exchange Gain/Loss Account in Company {},Ange Standard Valutaväxling Resultat Konto för Bolag {}, +Please set default Expense Account in Company {0},Ange Standard Konstnad Konto för Bolag {0}, +Please set default cost of goods sold account in company {0} for booking rounding gain and loss during stock transfer,Ange Standard Kostnad för sålda artiklar i bolag {0} för bokning av avrundning av vinst och förlust under lager överföring, +Please set either the Tax ID or Fiscal Code on Company '%s',"Ange antingen Skatt eller Moms Kod för Bolag ""%s""", +Please set filters,Ange Filter, +Please set one of the following:,Ange något av följande:, +Please set the cost center field in {0} or setup a default Cost Center for the Company.,Ange Resultat Enhet i {0} eller ange Standard Resultat Enhet för Bolag., +Please set {0} in BOM Creator {1},Ange {0} i Stycklista Generator {1}, +Please setup and enable a group account with the Account Type - {0} for the company {1},Konfigurera och aktivera Kontoplan Grupp med Kontoklass {0} för bolag {1}, +Please share this email with your support team so that they can find and fix the issue.,Dela detta e-post meddelande med support så att de kan hitta och åtgärda problem. , +Please specify a {0},Ange {0}, +Please try again in an hour.,Försök igen om en timme., +Please update Repair Status.,Uppdatera Reparation Status., +Portal User,Portal Användare, +Portal Users,Portal Användare, +Posting Datetime,Bokföring Datum och Tid, +Powered by {0},Tillhandahålls av {0}, +"Previous Year is not closed, please close it first","Föregående År är inte stängd, vänligen stäng det", +Price ({0}),Pris ({0}), +Price List Defaults,Prislista Standard, +Price Per Unit ({0}),Pris Per Enhet ({0}), +Primary Address and Contact,Primär Adress & Kontakt, +Primary Contact,Primär Kontakt, +Primary Party,Primär Parti, +Primary Role,Primär Roll, +Print Format Builder,Utskrift Format Redigerare, +Print Style,Utskrift Stil, +Printing,Utskrift, +Priority cannot be lesser than 1.,Prioritet får inte vara mindre än 1., +Priority is mandatory,Parti Erfodras , +Probability,Sannolikhet, +Process Loss,Process Förlust, +Process Loss Percentage cannot be greater than 100,Process Förlust i Procent får inte vara större än 100 , +Process Loss Qty,Process Förlust Kvantitet, +Process Loss Report,Process Förlust Rapport, +Process Loss Value,Process Förlust Värde, +Process Payment Reconciliation,Betalning Avstämning Bearbetning, +Process Payment Reconciliation Log,Betalning Avstämning Bearbetning Logg, +Process Payment Reconciliation Log Allocations,Betalning Avstämning Bearbetning Logg Tilldelningar, +Process Subscription,Behandla Prenumeration, +Process in Single Transaction,Process i Singel Transaktion, +Processed BOMs,Behandlade Stycklistor, +Processing Sales! Please Wait...,Behandlar Försäljning!Vänlige Vänta..., +Produced / Received Qty,Producerad / Mottagen Kvantitet, +Product Price ID,Artikel Pris, +Production Plan Already Submitted,Produktion Plan Redan Godkänd, +Production Plan Item Reference,Produktion Plan Post Referens, +Production Plan Qty,Produktion Plan Kvantitet, +Production Plan Sub Assembly Item,Produktion Plan Delmontering Artikel, +Production Plan Sub-assembly Item,Produktion Plan Delmontering Artikel, +Production Plan Summary,Produktion Plan Översikt, +Profile,Profil, +Profit and Loss Summary,Resultat Rapport Översikt, +Progress,Framsteg, +Progress (%),Framsteg(%), +Project Progress:,Projekt Framsteg:, +Prompt Qty,Fråga efter Kvantitet, +Prospect,Prospekt, +Prospect Lead,Prospekt Potentiell Kund, +Prospect Opportunity,Prospekt Möjlighet, +Prospect Owner,Prospekt Ansvarig, +Prospect {0} already exists,Prospekt {0} finns redan, +Provisional Account,Provisoriskt Konto, +Provisional Expense Account,Provisorisk Kostnad Konto, +Purchase Order Item reference is missing in Subcontracting Receipt {0},Inköp Order Artikel Referens saknas på Underleverantör Följesedel {0}, +Purchase Orders {0} are un-linked,Inköp Ordrar {0} är inte länkade, +Purchase Receipt (Draft) will be auto-created on submission of Subcontracting Receipt.,Inköp Följesedel (utkast) kommer att skapas automatiskt vid godkännande av Underleverantör Följesedel., +Purchase Receipt {0} created.,Inköp Följesedel {0} skapad, +Purchase Value,Inköp Värde, +Purchases,Inköp, +Purposes Required,Anledning Erfodras, +Putaway Rule,Lägg Undan Regel, +Putaway Rule already exists for Item {0} in Warehouse {1}.,Lägg Undan Regel finns redan för Artikel {0} i Lager {1}., +Qty ,Kvantitet, +Qty After Transaction,Kvantitet efter Transaktion, +Qty As Per BOM,Kvantitet per Stycklista, +Qty Change,Kvantitet Förändring, +Qty In Stock,Kvantitet i Lager, +Qty Per Unit,Kvantitet per Enhet, +"Qty To Manufacture ({0}) cannot be a fraction for the UOM {2}. To allow this, disable '{1}' in the UOM {2}.","Kvantitet att Producera ({0}) kan inte vara bråkdel för enhet {2}. För att tillåta detta, inaktivera '{1}' i enhet {2}.", +Qty To Produce,Kvantitet att Producera, +Qty and Rate,Kvantitet och Pris, +Qty as Per Stock UOM,Kvantitet per Lager Enhet, +Qty for which recursion isn't applicable.,Kvantitet för vilket rekursion inte är tillämplig., +Qty in Stock UOM,Kvantitet i Lager Enhet, +Qty of Finished Goods Item should be greater than 0.,Kvantitet Färdiga Artiklar ska vara högre än 0., +Qty to Be Consumed,Kvantitet att Förbruka, +Qty to Build,Kvantitet att Producera, +Qty to Fetch,Kvantitet att Hämta, +Qty to Produce,Kvantitet att Producera, +Qualification Status,Kvalificering Status, +Qualified,Kvalificerad, +Qualified By,Kvalificerad Av, +Qualified on,Kvalificerad(Datum), +Quality Inspection Parameter,Kvalitet Kontroll Parameter, +Quality Inspection Parameter Group,Kvalitet Kontroll Parametrar Grupp, +Quality Inspection Settings,Kvalitet Kontroll Inställningar, +Quality Inspection(s),Kvalitet Kontroll, +Quantity is required,Kvantitet erfodras, +"Quantity must be greater than zero, and less or equal to {0}",Kvantitet måste vara större än noll och mindre eller lika med {0}, +Quantity to Produce should be greater than zero.,Kvantitet att producera ska vara högre än noll., +Quantity to Scan,Kvantitet att Skanna, +Quarter {0} {1},Kvartal {0} {1}, +Quotation Number,Försäljning Offert Nummer, +Quoted Amount,Offererad Belopp, +Rate Difference with Purchase Invoice,Pris Differens mot Inköp Faktura, +Rate Section,Pris, +Rate of Stock UOM,Pris av Lager Enhet, +Ratios,Förhållanden, +Raw Material Cost Per Qty,Råmaterial Kostnad per Kvantitet, +Raw Material Item,Råmaterial Artikel, +Raw Material Value,Råmaterial Värde, +Raw Materials Actions,Råmaterial Åtgärder, +Raw Materials Consumption ,Råmaterial Förbrukning , +Raw Materials Warehouse,Råmaterial Lager, +Reached Root,Nått Rot, +Reading Value,Avläst Värde, +Reason for hold:,Anledning för Spärr:, +Rebuild Tree,Bygg om Träd, +Rebuilding BTree for period ...,Bygger om BTree för period ..., +Recalculate Incoming/Outgoing Rate,Räkna om In/Ut Pris, +Recalculating Purchase Cost against this Project...,Räkna om Inköp Kostnad mot detta Projekt..., +Receivable/Payable Account,Fordring / Skuld Konto, +Receivable/Payable Account: {0} doesn't belong to company {1},Fordring / Skuld Konto: {0} tillhör inte bolag {1}, +Received Amount After Tax,Mottaget Belopp Efter Moms, +Received Amount After Tax (Company Currency),Mottaget Belopp Efter Moms (Bolag Valuta), +Received Amount cannot be greater than Paid Amount,Mottaget Belopp kan inte vara högre än Betald Belopp, +Received Qty in Stock UOM,Mottagen Kvantitet (per Lager Enhet), +Recent Orders,Senaste Ordrar, +Recent Transactions,Senaste Transaktioner, +Reconcile All Serial Nos / Batches,Stäm av alla Serie Nummer / Partier, +Reconcile on Advance Payment Date,Stäm av på Förskott Betalning Datum, +Reconcile the Bank Transaction,Avstäm Bank Transaktion, +Reconciled Entries,Avstämda Poster, +Reconciliation Error Log,Avstämning Fel Logg, +Reconciliation Logs,Avstämning Logg, +Reconciliation Progress,Avstämning Framsteg, +Recording HTML,Inspelning HTML, +Recoverable Standard Rated expenses should not be set when Reverse Charge Applicable is Y,Återställbara Standard Klassade Kostnader ska inte anges när Omvänd Debitering är Ja, +Recurse Every (As Per Transaction UOM),Rekurs Varje (per Transaktion Enhet), +Recurse Over Qty cannot be less than 0,Rekurs Över Kvantitet får inte vara mindre än 0, +Recursive Discounts with Mixed condition is not supported by the system,Rekursiva Rabatter med Blandat Villkor stöds inte av system, +Reference Date for Early Payment Discount,Referens Datum för Tidig Betalning Rabatt, +Reference Detail,Referens Detalj, +Reference DocType,Referens DocType, +Reference Exchange Rate,Referens Växel Kurs, +Reference No,Referens Nummer. , +Reference number of the invoice from the previous system,Referens Nummer på Faktura från tidigare system, +References to Sales Invoices are Incomplete,Referenser till Försäljning Fakturor är ofullständiga, +References to Sales Orders are Incomplete,Referenser till Försäljning Ordrar är ofullständiga, +References {0} of type {1} had no outstanding amount left before submitting the Payment Entry. Now they have a negative outstanding amount.,Referenser {0} av typ {1} hade inget utestående belopp kvar innan godkännande av Betalning Post. Nu har de negativ utestående belopp., +Refresh Google Sheet,Uppdatera Google Sheet, +Refresh Plaid Link,Uppdatera Plaid Länk, +Regenerate Closing Stock Balance,Återskapa Stängning Lager Saldo, +Rejected Serial and Batch Bundle,Avvisad Serie och Parti Paket, +Rejected Warehouse and Accepted Warehouse cannot be same.,Avvisad lager och Accepterad lager kan inte vara samma., +Remarks Column Length,Anmärkningar Kolumn Bredd, +Remove Parent Row No in Items Table,Ta bort Överordnad Radnummer i Artikel Tabell, +Repair,Reparera, +Repair Asset,Reparera Tillgång, +Repair Details,Reparation Detaljer, +"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. +It also updates latest price in all the BOMs.","Ersätt stycklista i alla andra stycklistor där den används. Den kommer att ersätta gamla stycklista länk, uppdatera kostnaden och regenerera tabell ""Stycklista Utvidgad Artikel"" enligt ny stycklista. +Den uppdaterar också senaste pris i alla stycklistor.", +Report Error,Rapport Fel, +Report Filters,Rapport Sortering, +Report View,Rapport Vy, +Repost Accounting Ledger,Posta om Bokföring Register, +Repost Accounting Ledger Items,Posta om Bokföring Register Poster, +Repost Accounting Ledger Settings,Posta om Bokföring Register Poster Inställningar, +Repost Allowed Types,Posta om Tillåtna Typer, +Repost Error Log,Posta om Fel Logg, +Repost Item Valuation,Posta om Artikel Värdering, +Repost Payment Ledger,Posta om Betalning Register, +Repost Payment Ledger Items,Posta om Betalning Register Poster, +Repost has started in the background,Repost startad i bakgrund, +Repost in background,Repost i bakgrund, +Repost started in the background,Repost startad i bakgrund, +Reposting Completed {0}%,Repostering Klar {0}%, +Reposting Data File,Repostering av Data Fil, +Reposting Info,Repostering Info, +Reposting Progress,Repostering Framsteg, +Reposting entries created: {0},Repostering Poster skapade: {0}, +Reposting has been started in the background.,Repostering startad i bakgrund, +Reposting in the background.,Repostering i bakgrund, +Represents a Financial Year. All accounting entries and other major transactions are tracked against the Fiscal Year.,Representerar ett bokföringsår. Alla bokföringsposter och andra större transaktioner spåras mot bokföringsår., +Request Parameters,Begäran Parametrar, +Request Timeout,Begäran Löpte Ut, +Reservation Based On,Reservation Baserad På, +Reserve,Reservera, +Reserve Stock,Reservera, +"Reserved Qty ({0}) cannot be a fraction. To allow this, disable '{1}' in UOM {3}.","Reserverat Kvantitet ({0}) kan inte vara bråkdel. För att tillåta detta, inaktivera '{1}' i Enhet {3}.", +Reserved Qty for Production Plan,Reserverad Kvantitet för Produktion Plan, +Reserved Qty for Subcontract,Reserverad Kvantitet för Underleverantör, +Reserved Qty should be greater than Delivered Qty.,Reserverad Kvantitet ska vara högre än Levererad Kvantitet., +Reserved Serial No.,Reserverad Serie Nummer, +Reserved Stock,Reserverad, +Reserved Stock for Batch,Reserverad för Parti, +Reserved for POS Transactions,Reserverad för Kassa, +Reserved for Production,Reserverad för Produktion, +Reserved for Production Plan,Reserverad för Produktion Plan, +Reserved for Sub Contracting,Reserverad för Underleverantör, +Reserving Stock...,Reserverar...., +Reset Company Default Values,Återställ Bolag Standard Värde, +Reset Plaid Link,Återställ Plaid Länk, +Reset Raw Materials Table,Återställ Råmaterial Tabell, +Resolution Due,Beslut om, +Response and Resolution,Svar och Lösning, +Restart,Starta om, +Restore Asset,Återställ Tillgång, +Restrict,Begränsa, +Restrict Items Based On,Begränsa Artiklar Baserat På, +Result Key,Resultat Nyckel, +Resume Job,Återuppta Jobb, +Retried,Försökte igen, +Retry,Försök igen, +Retry Failed Transactions,Försök igen med Misslyckade Transaktioner, +Return Against,Retur Mot, +Return Against Subcontracting Receipt,Retur mot Underleverantör Följesedel, +Return Components,Returnera Komponenter, +Return Issued,Retur Utfärdad, +Return Qty,Retur Kvantitet, +Return Qty from Rejected Warehouse,Retur Kvantitet från Avvisad Lager, +Return of Components,Retur av Komponenter, +Returned,Retur, +Returned Against,Returnerad Mot, +Returned Qty ,Retur Kvantitet, +Returned Qty in Stock UOM,Retur Kvantitet (per Lager Enhet), +Returned exchange rate is neither integer not float.,Returnerad växelkurs är varken heltal eller flyttal., +Revaluation Journals,Omvärdering Journaler, +Revaluation Surplus,Omvärdering Överskott, +Revenue,Intäkt, +Reversal Of,Återföring Av, +Right Child,Höger Underordnad, +Role Allowed to Create/Edit Back-dated Transactions,Roll Godkänd att Skapa/Redigera Bakdaterade Transaktioner, +Role Allowed to Over Bill ,Roll Godkänd att Överfakturera, +Role Allowed to Over Deliver/Receive,Roll Godkänd att Över Leverera/Ta Emot, +Role Allowed to Override Stop Action,Roll Godkänd att Åsidosätta Stopp Åtgärd, +Role allowed to bypass Credit Limit,Roll Godkänd att Åsidosätta Kredit Gräns, +Root,Klass, +"Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity","Konto Klass för {0} måste vara en av följande klasser: Tillgång, Skuld, Intäkt, Kostnad och Eget Kapital", +Round Free Qty,Avrunda Gratis Kvantitet, +Round Off Tax Amount,Avrunda Moms Belopp, +Round Tax Amount Row-wise,Avrunda Moms Belopp per Artikelrad, +Rounding Loss Allowance,Avrundning Förlust Tillåtelse, +Rounding Loss Allowance should be between 0 and 1,Avrundning Förlust Tillåtelse ska vara mellan 0 och 1, +Rounding gain/loss Entry for Stock Transfer,Avrundning Resultat Post för Lager Överföring, +Row #,Rad #, +Row # {0}:,Rad # {0}:, +Row #{0}: A reorder entry already exists for warehouse {1} with reorder type {2}.,Rad # {0}: Återbeställning Post finns redan för lager {1} med återbeställning typ {2}., +Row #{0}: Acceptance Criteria Formula is incorrect.,Rad # {0}: Godkännande Villkor Formel är felaktig., +Row #{0}: Acceptance Criteria Formula is required.,Rad # {0}: Godkännande Villkor Formel erfodras., +Row #{0}: Accepted Warehouse and Rejected Warehouse cannot be same,Rad # {0}: Godkänd Lager och Avvisat Lager kan inte vara samma, +Row #{0}: Accepted Warehouse is mandatory for the accepted Item {1},Rad # {0}: Godkänd Lager erfodras för godkänd Artikel {1}, +Row #{0}: Allocated Amount cannot be greater than Outstanding Amount of Payment Request {1},Rad #{0}: Tilldelad belopp kan inte vara högre än utestående belopp för betalning begäran {1}, +Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3},Rad # {0}: Tilldela belopp:{1} är högre än utestående belopp:{2} för Betalning Villkor {3}, +Row #{0}: Amount must be a positive number,Rad # {0}: Belopp måste vara positiv tal, +Row #{0}: BOM is not specified for subcontracting item {0},Rad # {0}: Stycklista är inte specificerad för Underleverantör Artikel {0}, +Row #{0}: Batch No {1} is already selected.,Rad # {0}: Parti Nummer {1} är redan vald., +Row #{0}: Cannot allocate more than {1} against payment term {2},Rad # {0}: Kan inte tilldela mer än {1} mot betalning villkor {2}, +Row #{0}: Cannot transfer more than Required Qty {1} for Item {2} against Job Card {3},Rad # {0}: Kan inte överföra mer än Erforderlig Kvantitet {1} för Artikel {2} mot Jobbkort {3}, +Row #{0}: Consumed Asset {1} cannot be Draft,Rad # {0}: Förbrukad Tillgång {1} kan inte vara Utkast, +Row #{0}: Consumed Asset {1} cannot be cancelled,Rad # {0}: Förbrukad tillgång {1} kan inte annulleras, +Row #{0}: Consumed Asset {1} cannot be the same as the Target Asset,Rad # {0}: Förbrukad Tillgång {1} kan inte vara samma som Mål Tillgång, +Row #{0}: Consumed Asset {1} cannot be {2},Rad # {0}: Förbrukad Tillgång {1} kan inte vara {2}, +Row #{0}: Consumed Asset {1} does not belong to company {2},Rad # {0}: Förbrukad Tillgång {1} tillhör inte Bolag {2}, +Row #{0}: Cumulative threshold cannot be less than Single Transaction threshold,Rad # {0}: Kumulativ tröskel får inte vara lägre än Enskild Transaktion tröskel, +Row #{0}: Dates overlapping with other row,Rad # {0}: Datum överlappar andra rad , +Row #{0}: Default BOM not found for FG Item {1},Rad # {0}: Standard Stycklista hittades inte för Färdig Artikel {1} , +Row #{0}: Expense Account not set for the Item {1}. {2},Rad # {0}: Kostnad Konto inte angiven för Artikel {1}. {2}, +Row #{0}: Finished Good Item Qty can not be zero,Rad # {0}: Färdig Artikel Kvantitet kan inte vara noll, +Row #{0}: Finished Good Item is not specified for service item {1},Rad # {0}: Färdig Artikel är inte specificerad för Service Artikel {1} , +Row #{0}: Finished Good Item {1} must be a sub-contracted item,Rad # {0}: Färdig Artikel {1} måste vara Underleverantör Artikel , +Row #{0}: Finished Good reference is mandatory for Scrap Item {1}.,Rad # {0}: Färdig Artikel referens erfodras för Rest Artikel {1}. , +Row #{0}: For {1} Clearance date {2} cannot be before Cheque Date {3},Rad #{0}: För {1} Avstämning datum {2} kan inte vara före Check Datum {3}, +"Row #{0}: For {1}, you can select reference document only if account gets credited",Rad # {0}: För {1} kan du välja referens dokument endast om konto krediteras, +"Row #{0}: For {1}, you can select reference document only if account gets debited",Rad # {0}: För {1} kan du välja referens dokument endast om konto debiteras, +Row #{0}: From Date cannot be before To Date,Rad # {0}: Från Datum kan inte vara före Till Datum, +Row #{0}: Item {1} does not exist,Rad # {0}: Artikel {1} finns inte, +"Row #{0}: Item {1} has been picked, please reserve stock from the Pick List.","Rad # {0}: Artikel {1} är plockad, reservera lager från Plocklista. ", +Row #{0}: Item {1} is not a service item,Rad # {0}: Artikel {1} är inte service artikel, +Row #{0}: Item {1} is not a stock item,Rad # {0}: Artikel {1} är inte service artikel, +Row #{0}: Only {1} available to reserve for the Item {2},Rad # {0}: Endast {1} tillgänglig att reservera för artikel {2} , +Row #{0}: Please select Item Code in Assembly Items,Rad # {0}: Välj Artikel Kod för Montering Artiklar, +Row #{0}: Please select the BOM No in Assembly Items,Rad # {0}: Välj Stycklista Nummer för Montering Artiklar, +Row #{0}: Please select the Sub Assembly Warehouse,Rad # {0}: Välj Undermontering Lager, +Row #{0}: Please update deferred revenue/expense account in item row or default account in company master,Rad # {0}: Uppdatera konto för uppskjutna intäkter/kostnader i artikel rad eller standard konto i bolag, +Row #{0}: Qty increased by {1},Rad # {0}: Kvantitet ökade med {1}, +Row #{0}: Qty must be a positive number,Rad # {0}: Kvantitet måste vara psitivt tal, +Row #{0}: Qty should be less than or equal to Available Qty to Reserve (Actual Qty - Reserved Qty) {1} for Iem {2} against Batch {3} in Warehouse {4}.,Rad # {0}: Kvantitet ska vara mindre än eller lika med tillgänglig kvantitet att reservera (verklig antal - reserverad antal) {1} för artikel {2} mot parti {3} i lager {4}., +Row #{0}: Quantity to reserve for the Item {1} should be greater than 0.,Rad # {0}: Kvantitet att reservera för Artikel {1} ska vara högre än 0., +Row #{0}: Rate must be same as {1}: {2} ({3} / {4}),Rad #{0}: Pris måste vara samma som {1}: {2} ({3} / {4}) , +Row #{0}: Received Qty must be equal to Accepted + Rejected Qty for Item {1},Rad #{0}: Mottaget Kvantitet måste vara lika med Godkänd + Avvisad Kvantitet för Artikel {1}, +Row #{0}: Rejected Qty cannot be set for Scrap Item {1}.,Rad # {0}: Avvisad Kvantitet kan inte anges för Rest Artikel {1}., +Row #{0}: Rejected Warehouse is mandatory for the rejected Item {1},Rad # {0}: Avvisad Lager erfodras för avvisad Artikel {1}, +Row #{0}: Scrap Item Qty cannot be zero,Rad # {0}: Rest Artikel Kvantitet kan inte vara noll, +"Row #{0}: Selling rate for item {1} is lower than its {2}. + Selling {3} should be atleast {4}.

Alternatively, + you can disable selling price validation in {5} to bypass + this validation.","Rad # {0}: Försäljning Pris för artikel {1} är lägre än {2}. + Försäljning Pris {3} ska vara minst {4}.

Alternativt, + kan man inaktivera validering av försäljning pris i {5} för att kringgå + denna validering.", +Row #{0}: Serial No {1} for Item {2} is not available in {3} {4} or might be reserved in another {5}.,Rad # {0}: Serie Nummer {1} för artikel {2} är inte tillgänglig i {3} {4} eller kan vara reserverad i annan {5}., +Row #{0}: Serial No {1} is already selected.,Rad # {0}: Serie Nummer {1} är redan vald., +Row #{0}: Start Time and End Time are required,Rad # {0}: Från Tid och till Tid erfodras. , +Row #{0}: Start Time must be before End Time,Rad # {0}: Från Tid måste vara före till Tid , +Row #{0}: Status is mandatory,Rad # {0}: Status erfodras, +Row #{0}: Stock cannot be reserved for Item {1} against a disabled Batch {2}.,Rad # {0}: Lager kan inte reserveras för artikel {1} mot inaktiverad Parti {2}., +Row #{0}: Stock cannot be reserved for a non-stock Item {1},Rad # {0}: Lager kan inte reserveras för artikel som inte finns i lager {1}, +Row #{0}: Stock cannot be reserved in group warehouse {1}.,Rad # {0}: Lager kan inte reserveras i Grupp Lager {1}., +Row #{0}: Stock is already reserved for the Item {1}.,Rad # {0}: Lager är redan reserverad för artikel {1}., +Row #{0}: Stock is reserved for item {1} in warehouse {2}.,Rad # {0}: Lager är reserverad för artikel {1} i lager {2}., +Row #{0}: Stock not available to reserve for Item {1} against Batch {2} in Warehouse {3}.,Rad # {0}: Lager är inte tillgänglig att reservera för artikel {1} mot Parti {2} i Lager {3}., +Row #{0}: Stock not available to reserve for the Item {1} in Warehouse {2}.,Rad # {0}: Kvantitet ej tillgänglig för reservation för Artikel {1} på {2} Lager., +Row #{0}: The warehouse {1} is not a child warehouse of a group warehouse {2},Rad # {0}: Lager {1} är inte underordnad till grupp lager {2}, +Row #{0}: You cannot use the inventory dimension '{1}' in Stock Reconciliation to modify the quantity or valuation rate. Stock reconciliation with inventory dimensions is intended solely for performing opening entries.,Rad # {0}: Man kan inte använda Lager Dimension '{1}' i Lager Avstämning för att ändra kvantitet eller Grund Pris. Lager Avstämning med Lager Dimensioner är endast avsedd för att utföra öppningsposter., +Row #{0}: You must select an Asset for Item {1}.,Rad # {0}: Du måste välja Tillgång för Artikel {1}., +Row #{0}: {1} is not a valid reading field. Please refer to the field description.,Rad #{0}: {1} är inte giltigt läsfält. Se fält beskrivning., +Row #{0}: {1} of {2} should be {3}. Please update the {1} or select a different account.,Rad # {0}: {1} av {2} ska vara {3}. Uppdatera {1} eller välj ett annat konto., +Row #{1}: Warehouse is mandatory for stock Item {0},Rad # {1}: Lager erfodras för lager artikel {0} , +Row #{}: Finance Book should not be empty since you're using multiple.,Rad # {}: Finans Register ska inte vara tom eftersom du använder flera., +Row #{}: Please use a different Finance Book.,Rad # {}: Använd annan Finans Register., +Row #{}: The original Invoice {} of return invoice {} is not consolidated.,Rad #{}: Ursprunglig Faktura {} för Retur Faktura {} är inte konsoliderad., +Row #{}: item {} has been picked already.,Rad # {}: Artikel {} är redan plockad., +Row #{}: {} {} doesn't belong to Company {}. Please select valid {}.,Rad # {}: {} {} tillhör inte bolag {}. Välj giltig {}., +Row No {0}: Warehouse is required. Please set a Default Warehouse for Item {1} and Company {2},Rad # {0}: Lager erfodras. Ange Standard Lager för Artikel {1} och Bolag {2}, +Row Number,Rad Nummer, +Row {0},Rad {0}, +"Row {0} picked quantity is less than the required quantity, additional {1} {2} required.","Rad # {0}: Plockad kvantitet är lägre än erfodrad kvantitet, ytterligare {1} {2} erfodras.", +Row {0}# Item {1} cannot be transferred more than {2} against {3} {4},Rad # {0}: Artikel {1} kan inte överföras mer än {2} mot {3} {4}, +Row {0}# Item {1} not found in 'Raw Materials Supplied' table in {2} {3},"Rad # {0}: Artikel {1} hittades inte i tabellen ""Råmaterial Levererad"" i {2} {3}", +Row {0}: Accepted Qty and Rejected Qty can't be zero at the same time.,Rad # {0}: Godkänd Kvantitet och Avvisad Kvantitet kan inte vara noll samtidigt., +Row {0}: Account {1} and Party Type {2} have different account types,Rad # {0}: Konto {1} och Parti Typ {2} har olika konto typer, +Row {0}: Allocated amount {1} must be less than or equal to invoice outstanding amount {2},Rad # {0}: Tilldelad belopp {1} måste vara lägre än eller lika med utestående faktura belopp {2}, +Row {0}: Allocated amount {1} must be less than or equal to remaining payment amount {2},Rad # {0}: Tilldelad belopp {1} måste vara lägre än eller lika med återstående betalning belopp {2}, +"Row {0}: As {1} is enabled, raw materials cannot be added to {2} entry. Use {3} entry to consume raw materials.",Rad {0}: Eftersom {1} är aktiverat kan råmaterial inte läggas till {2} post. Använd {3} post för att förbruka råmaterial., +Row {0}: Both Debit and Credit values cannot be zero,Rad # {0}: Både debet och kredit värdena kan inte vara noll, +Row {0}: Cost Center {1} does not belong to Company {2},Rad # {0}: Resultat Enhet {1} tillhör inte Bolag {2}, +Row {0}: Either Delivery Note Item or Packed Item reference is mandatory.,Rad # {0}: Antingen Följesedel eller Packad Artikel Referens erfordras, +Row {0}: Expense Head changed to {1} as no Purchase Receipt is created against Item {2}.,Rad # {0}: Kostnad har ändrats till {1} eftersom inget Inköp Följesedel är skapad mot Artikel {2}., +Row {0}: Expense Head changed to {1} because account {2} is not linked to warehouse {3} or it is not the default inventory account,Rad # {0}: Kostnad har ändrats till {1} eftersom konto {2} inte är länkat till lager {3} eller det inte är standard konto för lager, +Row {0}: Expense Head changed to {1} because expense is booked against this account in Purchase Receipt {2},Rad # {0}: Kostnad har ändrats till {1} eftersom kostnad bokförs mot detta konto i Inköp Följesedel {2}, +Row {0}: From Warehouse is mandatory for internal transfers,Rad # {0}: Från Lager erfodras för interna överföringar, +Row {0}: Item Tax template updated as per validity and rate applied,Rad # {0}: Artikel Moms Mall uppdaterad enligt giltighet och tillämpad moms sats, +Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer,Rad # {0}: Artikel Pris är uppdaterad enligt Grund Pris eftersom det är intern lager överföring, +Row {0}: Item {1} must be a stock item.,Rad # {0}: Artikel {1} måste vara lager artikel., +Row {0}: Item {1} must be a subcontracted item.,Rad # {0}: Artikel {1} måste vara Underleverantör Artikel., +Row {0}: Packed Qty must be equal to {1} Qty.,Rad # {0}: Packad Kvantitet måste vara lika med {1} Kvantitet., +Row {0}: Packing Slip is already created for Item {1}.,Rad # {0}: Packsedel är redan skapad för Artikel {1}., +Row {0}: Payment Term is mandatory,Rad # {0}: Betalning Villkor Erfodras, +Row {0}: Please provide a valid Delivery Note Item or Packed Item reference.,Rad # {0}: Ange giltig referens för Försäljning Följesedel eller Packsedel., +Row {0}: Please select a BOM for Item {1}.,Rad # {0}: Välj Stycklista för Artikel {1}., +Row {0}: Please select an active BOM for Item {1}.,Rad # {0}: Välj aktiv Stycklista för Artikel {1}., +Row {0}: Please select an valid BOM for Item {1}.,Rad # {0}: Välj giltig Stycklista för Artikel {1}, +Row {0}: Project must be same as the one set in the Timesheet: {1}.,Rad # {0}: Projekt måste vara samma som är angiven i tidrapport: {1}., +Row {0}: Purchase Invoice {1} has no stock impact.,Rad # {0}: Inköp Faktura {1} har ingen efekt på lager., +Row {0}: Qty cannot be greater than {1} for the Item {2}.,Rad # {0}: Kvantitet får inte vara högre än {1} för Artikel {2}., +Row {0}: Qty in Stock UOM can not be zero.,Rad # {0}: Kvantitet i Lager Enhet kan inte vara noll., +Row {0}: Qty must be greater than 0.,Rad # {0}: Kvantitet måste vara högre än 0., +Row {0}: Shift cannot be changed since the depreciation has already been processed,Rad # {0}: Förskjutning inte ändras eftersom avskrivning redan är behandlad, +Row {0}: Target Warehouse is mandatory for internal transfers,Rad # {0}: Till Lager erfodras för interna överföringar, +"Row {0}: To set {1} periodicity, difference between from and to date must be greater than or equal to {2}",Rad # {0}: För att ange periodicitet för {1} måste skillnaden mellan från och till datum vara större än eller lika med {2}, +Row {0}: Total Number of Depreciations cannot be less than or equal to Opening Number of Booked Depreciations,Rad # {0}: Totalt Antal Avskrivningar får inte vara mindre än eller lika med antal bokförda avskrivningar, +Row {0}: {1} account already applied for Accounting Dimension {2},Rad # {0}: {1} konto är redan tillämpad för Bokföring Dimension {2}, +Row {0}: {1} {2} cannot be same as {3} (Party Account) {4},Rad # {0}: {1} {2} kan inte vara samma som {3} (Parti Konto) {4}, +Row {0}: {2} Item {1} does not exist in {2} {3},Rad # {0}: {2} Artikel {1} finns inte i {2} {3}, +Row({0}): Outstanding Amount cannot be greater than actual Outstanding Amount {1} in {2},Rad # ({0}): Utestående belopp kan inte vara högre än verklig utestående belopp {1} i {2}, +Rows with Same Account heads will be merged on Ledger,Rader med samma Konto Poster kommer slås samman i Bokföring Register, +Rows: {0} have 'Payment Entry' as reference_type. This should not be set manually.,"Rader: {0} har ""Betalning Post"" som referens typ. Detta ska inte anges manuellt.", +Rows: {0} in {1} section are Invalid. Reference Name should point to a valid Payment Entry or Journal Entry.,Rader: {0} i sektion {1} är ogiltiga. Referens namn ska peka på giltig Betalning Post eller Journal Post, +Run parallel job cards in a workstation,Kör parallella jobbkort på arbetsplats, +Running,Behandlar, +SCO Supplied Item,Underleverantör Levererad Artikel, +SLA Fulfilled On,Service Nivå Avtal Uppfylld , +SLA Fulfilled On Status,Service Nivå Avtal Uppfylld Status, +SLA Paused On,Service Nivå Avtal Pausad, +SLA will be applied if {1} is set as {2}{3},"Service Nivå Avtal kommer att tillämpas om {1} är angiven som {2}{3} +​", +SLA will be applied on every {0},Service Nivå Avtal kommer att tillämpas varje {0}, +SMS Settings,SMS Inställningar, +SO Total Qty,Försäljning Order Totalt Kvantitet, +Salary Currency,Lön Valuta, +Sales Incoming Rate,Inkommande Försäljning Pris, +Sales Invoice {0} must be deleted before cancelling this Sales Order,Försäljning Faktura {0} måste annulleras innan annullering av denna Försäljning Order, +Sales Order Packed Item,Försäljning Order Packad Artikel, +Sales Order Reference,Försäljning Order Referens, +Sales Order Status,Försäljning Order Status, +"Sales Order {0} already exists against Customer's Purchase Order {1}. To allow multiple Sales Orders, Enable {2} in {3}","Försäljning Order {0} finns redan mot Kund Inköp Order {1}. För att tillåta flera Försäljning Ordrar, aktivera {2} i {3}", +Sales Partner ,Försäljning Partner, +Sales Partner Item,Försäljning Partner Artikel, +Sales Partner Target Variance Based On Item Group,Artikel Grupp Mål Avvikelse per Försäljning Partner, +Sales Pipeline Analytics,Försäljning Analys, +Sales Update Frequency in Company and Project,Försäljning Uppdatering Intervall, +Sales Value,Försäljning Värde, +Salvage Value Percentage,Procentuellt Rest Värde, +Same item and warehouse combination already entered.,Samma artikel och lager kombination är redan angivna., +Savings,Besparingar, +Scan Batch No,Skanna Parti Nummer, +Scan Mode,Skanning Läge, +Scan Serial No,Skanna Serie Nummer, +Scan barcode for item {0},Skanna Streckkod för artikel {0}, +"Scan mode enabled, existing quantity will not be fetched.","Skanning Läge aktiverad, befintlig kvantitet kommer inte att hämtas.", +Scanned Quantity,Skannad Kvantitet, +Scheduled Time Logs,Schemalagda Tidsloggar, +Scheduler is Inactive. Can't trigger job now.,Schemaläggare är inaktiv. Kan inte starta jobb nu., +Scheduler is Inactive. Can't trigger jobs now.,Schemaläggare är inaktiv. Kan inte starta jobb nu., +Scheduler is inactive. Cannot enqueue job.,Schemaläggare är inaktiv. Kan inte placera jobb i kö., +Scheduler is inactive. Cannot merge accounts.,Schemaläggare är inaktiv. Kan inte slå samman konton., +Scheduling,Schemaläggning, +"Scorecard variables can be used, as well as: +{total_score} (the total score from that period), +{period_number} (the number of periods to present day) +","Resultatkort variabler kan användas, såväl som: +{total_score} (totalt resultat från detta period), +{period_number} (antal intervall tills nu) +", +Scrap & Process Loss,Rest & Bearbetning Förlust, +Scrap Asset,Rest Tillgång, +Scrap Cost Per Qty,Rest Kostnad per Kvantitet, +Scrap Item Code,Rest Artikel Kod, +Scrap Item Name,Rest Artikel Namn, +"Search by item code, serial number or barcode","Sök efter Artikel Kod, Serie Nummer eller Streck/QR Kod", +Secondary Party,Sekundär Parti, +Secondary Role,Sekundär Roll, +Segregate Serial / Batch Bundle,Skilj Serie / Parti Paket, +Select Accounting Dimension.,Välj Bokföring Dimension, +Select Alternative Items for Sales Order,Välj Alternativ Artikel för Försäljning Order, +Select Batch No,Välj Parti Nummer, +Select Corrective Operation,Välj Korrigerande Åtgärd, +Select Date of Birth. This will validate Employees age and prevent hiring of under-age staff.,Välj Födelsedag. Detta kommer att validera personal ålder och förhindra anställning av minderårig personal., +"Select Date of joining. It will have impact on the first salary calculation, Leave allocation on pro-rata bases.","Välj Anställning Datum. Detta kommer att påverka första lön, Ledighet tilldelning på proportionell bas.", +Select Dimension,Välj Dimension, +Select Finished Good,Välj Färdig Artikel, +Select Items for Quality Inspection, Välj Artiklar för Kvalitet Kontroll, +Select Job Worker Address,Välj Jobb Ansvarig Adress, +Select Serial No,Välj Serie Nummer, +Select Serial and Batch,Välj Serie Nummer och Parti Nummer, +Select Time,Välj Tid, +Select View,Välj Vy, +Select Vouchers to Match,Välj Verifikat, +Select Warehouses to get Stock for Materials Planning,Välj Lager för att hämta Lager Kvantitet för Material Planering, +Select a Company this Employee belongs to.,Välj Bolag som detta Personal tillhör till, +Select a Customer,Välj Kund, +Select an Item Group.,Välj Artikel Grupp, +Select an invoice to load summary data,Välj faktura för att ladda översikt data, +Select an item from each set to be used in the Sales Order.,Välj artikel från varje uppsättning som ska användas i Försäljning Order., +Select the Default Workstation where the Operation will be performed. This will be fetched in BOMs and Work Orders.,Välj Standard Arbetsstation där Åtgärd ska utföras. Detta kommer att läggas till Stycklistor och Arbetsordrar., +Select the Item to be manufactured.,Välj Artikel som ska produceras., +"Select the Item to be manufactured. The Item name, UoM, Company, and Currency will be fetched automatically.","Välj Artikel som ska produceras. Artikel Namn, Enhet, Bolag och Valuta kommer att hämtas automatiskt.", +Select the Warehouse,Välj Lager, +Select the date and your timezone,Välj Datum och Tidzon, +Select the raw materials (Items) required to manufacture the Item,Välj Råmaterial (Artiklar) som erfodras för att producera artikel, +"Select whether to get items from a Sales Order or a Material Request. For now select Sales Order. + A Production Plan can also be created manually where you can select the Items to manufacture.","Välj att få artiklar från Försäljning Order eller Material Begäran. För Tillfället Välj Försäljning Order. + Produktion Plan kan också skapas manuellt där man kan välja vilka artiklar som ska produceras.", +Select your weekly off day,Välj ledig dag i veckan, +Selected Vouchers,Valda Verifikat, +Selected date is,Vald Datum, +Selected document must be in submitted state,Vald dokument måste ha godkänd status, +Self delivery,Egen Leverans, +Sell Asset,Sälj Tillgång, +Send Attached Files,Skicka Bifogade Filer, +Send Document Print,Skicka Utskrifter, +Send Emails,Skicka E-post, +Sequential,Sekventiell, +Serial & Batch Item,Serie Nummer & Parti, +Serial & Batch Item Settings,Serie Nummer & Parti Inställningar, +Serial / Batch Bundle,Serie / Parti Paket, +Serial / Batch Bundle Missing,Serie / Parti Paket Saknas, +Serial / Batch No,Serie / Parti Nummer, +Serial / Batch Nos,Serie / Parti Nummer, +Serial No Ledger,Serie Nummer Register, +Serial No Range,Serienummer Intervall, +Serial No and Batch Selector cannot be use when Use Serial / Batch Fields is enabled.,Serie Nummer och Parti Väljare kan inte användas när Använd Serie Nummer / Parti Fält är aktiverad., +Serial No and Batch for Finished Good,Serie Nummer och Parti för Färdig Artikel, +Serial No is mandatory,Serie Nummer erfodras, +Serial No {0} already exists,Serie Nummer {0} finns redan, +Serial No {0} already scanned,Serie Nummer {0} är redan skannad, +Serial No {0} does not exists,Serie Nummer {0} finns inte , +Serial No {0} is already added,Serie Nummer {0} har redan lagts till, +Serial Nos,Serie Nummer., +Serial Nos / Batch Nos,Serie Nummer. / Parti Nummer., +Serial Nos Mismatch,Serie Nummer stämmer inte, +Serial Nos are created successfully,Serie Nummer skapade, +"Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding.","Serie Nmmer är reserverade iLagerreservationsinlägg, du måste avboka dem innan du fortsätter.", +Serial and Batch,Serie Nummer och Parti , +Serial and Batch Bundle,Serie och Parti Paket, +Serial and Batch Bundle created,Serie och Parti Paket skapad, +Serial and Batch Bundle updated,Serie och Parti Paket uppdaterad, +Serial and Batch Bundle {0} is already used in {1} {2}.,Serie och Parti Paket {0} används redan i {1} {2}., +Serial and Batch Details,Serie och Parti Detaljer, +Serial and Batch Entry,Serie och Parti Post, +Serial and Batch No,Serie och Parti Nummer, +Serial and Batch Nos,Serie och Parti Nummer, +Serial and Batch Nos will be auto-reserved based on Pick Serial / Batch Based On,Serie och Parti Nummer kommer att reserveras automatiskt baserat på Välj Serie/Parti baserat på, +Serial and Batch Reservation,Serie Nummer och Parti Reservation, +Serial and Batch Summary,Serie- och batchnummer kommer att reserveras automatiskt baserat på Välj serie/batch baserat på, +Service Cost Per Qty,Service Kostnad Per Kvantitet, +Service Expense Total Amount,Service Kostnad Totalt Belopp, +Service Expenses,Service Kostnader, +Service Item,Service Artikel, +Service Item Qty,Service Artikel Kvantitet, +Service Item Qty / Finished Good Qty,Service Artikel Kvantitet / Färdig Artikel Kvantitet, +Service Item UOM,Service Artikel Enhet, +Service Item {0} is disabled.,Service Artikel är inaktiverad, +Service Item {0} must be a non-stock item.,Service Artikel {0} får inte vara Lager Artikel., +Service Items,Service Artikel, +Service Level Agreement for {0} {1} already exists.,Service Nivå Avtal för {0} {1} finns redan., +Service Level Name,Service Nivå Namn, +Service Provider,Service Leverantör, +Set Default Supplier,Ange Standard Leverantör, +Set From Warehouse,Ange Från Lager, +Set Landed Cost Based on Purchase Invoice Rate,Ange Landad Kostnad baserat på Inköp Faktura Pris, +Set Loyalty Program,Ange Bonus Program, +Set Operating Cost / Scrape Items From Sub-assemblies,Ange Driftskostnad / Rest Artiklar från Underenheter, +Set Operating Cost Based On BOM Quantity,Ange Åtgärd Kostnad baserad på Stycklista, +Set Parent Row No in Items Table,Ange Överordnad Radnummer i Artikel Tabell, +Set Process Loss Item Quantity,Ange Process Förlust Artikel Kvantitet, +Set Project Status,Ange Projekt Status, +Set Quantity,Ange Kvantitet, +Set Response Time for Priority {0} in row {1}.,Ange Svarstid för Prioritet {0} i rad {1}., +Set Valuation Rate Based on Source Warehouse,Ange Grund Pris Baserad på Från Lager, +Set Warehouse,Välj Lager, +Set default {0} account for non stock items,Ange Standard {0} konto för Ej Lager Artiklar, +Set fieldname from which you want to fetch the data from the parent form.,Ange fältnamn från vilket data ska hämtas från överordnad formulär., +Set quantity of process loss item:,Ange kvantitet för Process Förlust Artikel:, +Set the Planned Start Date (an Estimated Date at which you want the Production to begin),Ange Planerad Start Datum, +Set the status manually.,Ange status manuellt., +Set {0} in asset category {1} for company {2},Ange {0} i Tillgång Kategori {1} för Bolag {2}, +Sets 'Accepted Warehouse' in each row of the Items table.,Ange 'Godkänd Lager' i varje rad i Artiklar Tabell, +Sets 'Rejected Warehouse' in each row of the Items table.,Ange 'Avvisad Lager' i varje rad i Artikel Tabell, +Sets 'Reserve Warehouse' in each row of the Supplied Items table.,Anger 'Reserv Lager' i varje rad i Levererad Artikel Tabell., +Setting Item Locations...,Anger Artikelplatser..., +Setting the account as a Company Account is necessary for Bank Reconciliation,Ange konto som Bolag Konto för Bank Avstämmning, +Setting {} is required,Konfigurera {} erfodras, +Setup your organization,Bolag Inställningar, +Shelf Life in Days,Hållbarhet i Dagar, +Shift,Skift, +Shift Factor,Förskjutning Faktor, +Shift Name,Förskjutning Namn, +Shipment,Leverans, +Shipment Amount,Leverans Belopp, +Shipment Delivery Note,Leverans Försäljning Följesedel, +Shipment ID,Leverans ID, +Shipment Information,Leverans Information, +Shipment Parcel,Leverans Paket, +Shipment Parcel Template,Leverans Paket Mall, +Shipment Type,Leverans Typ, +Shipment details,Leverans Detaljer, +Shipping Address Details,Leverans Adress Detaljer, +Shipping Address Template,Leverans Adress Mall, +Show Balances in Chart Of Accounts,Visa Saldo i Kontoplan, +Show Barcode Field in Stock Transactions,Visa Streck/QR Kod Fält i Lager Transaktioner, +Show Dimension Wise Stock,Visa Lager per Dimension, +Show Disabled Warehouses,Visa Inaktiverade Lager, +Show Failed Logs,Visa Misslyckade Logg, +Show GL Balance,Visa Bokföring Register Saldo, +Show Item Name,Visa Artikel Namn, +Show Ledger View,Visa Register Vy, +Show Net Values in Party Account,Visa Nettovärde i Parti Konto, +Show Pay Button in Purchase Order Portal,Visa Betala Knapp i Portal, +Show Preview,Förhandsgranska, +Show Remarks,Visa Anmärkningar, +Show Taxes as Table in Print,Visa Moms Belopp som Kolumn, +Show net values in opening and closing columns,Visa Nettovärde i Öppning och Stängning kolumner, +Show only the Immediate Upcoming Term,Visa endast Omedelbart Kommande Villkor, +Show pending entries,Visa väntande poster, +Show with upcoming revenue/expense,Visa med kommande Intäkter/Kostnader, +"Simple Python Expression, Example: doc.status == 'Open' and doc.issue_type == 'Bug'","Enkelt Python uttryck, exempel: doc.status == 'Open' och doc.issue_type == 'Bug'", +"Simple Python formula applied on Reading fields.
Numeric eg. 1: reading_1 > 0.2 and reading_1 < 0.5
+Numeric eg. 2: mean > 3.5 (mean of populated fields)
+Value based eg.: reading_value in (""A"", ""B"", ""C"")","Enkel Python formel tillämpad på läsfält.
Numerisk t.ex. 1: reading_1 > 0.2 and reading_1 < 0.5
+Numerisk t.ex. 2: mean > 3.5 (mean of populated fields)
+Värde baserad t.ex.: reading_value in (""A"", ""B"", ""C"")", +Simultaneous,Samtidig, +Skip Available Sub Assembly Items,Hoppa över tillgängliga Delmontering Artiklar, +Skipped,Hoppade över, +Skipping Tax Withholding Category {0} as there is no associated account set for Company {1} in it.,Hoppar över Moms Undantag Kategori {0} eftersom det inte finns något konto associerat med Bolag {1}., +"Skipping {0} of {1}, {2}","Hoppa över {0} av {1}, {2}", +Sold by,Försäljare, +Something went wrong please try again,Något gick snett! Försök igen., +Source Exchange Rate,Käll Växelkurs, +Source Fieldname,Käll Fältnamn, +Source Warehouse Address Link,Från Lager Adress Länk, +South Africa VAT Account,Sydafrika Moms Konto, +South Africa VAT Settings,Sydafrika Moms Konto Inställningar, +Spacer,Mellanrum, +Split Asset,Dela Tillgång, +Split Early Payment Discount Loss into Income and Tax Loss,Dela Tidig Betalning Rabatt Bortfall i Intäkt och Moms Bortfall, +Split From,Dela Från, +Split Qty,Dela Kvantitet, +Split qty cannot be grater than or equal to asset qty,Delad kvantitet får inte vara större än eller lika med tillgång kvantitet, +Splitting {0} {1} into {2} rows as per Payment Terms,Delar {0} {1} i {2} rader enligt Betalning Villkor, +Stage,Stadie, +Stale Days should start from 1.,Inaktuella Dagar ska börja från 1., +Standard Description,Standard Beskrivning, +Standard Rated Expenses,Standard Klassade Kostnader, +"Standard Terms and Conditions that can be added to Sales and Purchases. Examples: Validity of the offer, Payment Terms, Safety and Usage, etc.","Standard Villkor som kan läggas till Försäljning och Inköp. Exempel: Erbjudande Giltighet, Betalningsvillkor, Säkerhet,Användning, etc.", +Standard rated supplies in {0},Standard Klassade Förbrukning Artiklar i {0}, +"Standard tax template that can be applied to all Purchase Transactions. This template can contain a list of tax heads and also other expense heads like ""Shipping"", ""Insurance"", ""Handling"", etc.","Standard Moms Mall som kan tillämpas på alla Inköp Transaktioner. Mallen kan innehålla lista över Moms Konto och även andra kostnad konto som ""Leverans"", ""Försäkring"", ""Hantering"" osv.", +"Standard tax template that can be applied to all Sales Transactions. This template can contain a list of tax heads and also other expense/income heads like ""Shipping"", ""Insurance"", ""Handling"" etc.","Standard Moms Mall som kan tillämpas på alla Försäljning Transaktioner. Mallen kan innehålla lista över Moms Konto och även andra kostnad konto som ""Leverans"", ""Försäkring"", ""Hantering"" osv.", +Start / Resume,Starta / Återuppta, +Start Date should be lower than End Date,Startdatum ska vara före Slutdatum, +Start Deletion,Starta Borttagning, +Start Import,Starta Import, +Start Job,Starta Jobb, +Start Merge,Starta Sammanslagning, +Start Reposting,Starta Ompostning, +Start Time can't be greater than or equal to End Time for {0}.,Start Tid får inte vara senare än eller lika med Slut Tid för {0}., +Started a background job to create {1} {0},Startade bakgrundsjobb för att skapa {1} {0}, +Status Details,Status Detaljer, +Status Illustration,Statusbild, +Status set to rejected as there are one or more rejected readings.,Status satt till avvisad eftersom det finns en eller flera avvisade avläsningar., +Stock Closing,Lager Låsning, +Stock Consumed During Repair,Lager Förbrukad under Reparation, +Stock Consumption Details,Lager Förbrukning Detaljer, +Stock Entries already created for Work Order {0}: {1},Lager Poster redan skapade för Arbetsorder {0}: {1}, +Stock Ledger Invariant Check,Lager Register Oföränderlig Kontroll, +Stock Ledger Variance,Lager Register Avvikelse, +Stock Movement,Lager Hantering, +Stock Planning,Lager Planering, +Stock Reposting Settings,Lager Ompostering Inställningar, +Stock Reservation,Lager Reservation, +Stock Reservation Entries Cancelled,Lager Reservation Poster Annullerade, +Stock Reservation Entries Created,Lager Reservation Poster Skapade, +Stock Reservation Entry,Lager Reservation Post, +Stock Reservation Entry cannot be updated as it has been delivered.,Lager Reservation Post kan inte uppdateras eftersom den är levererad. , +"Stock Reservation Entry created against a Pick List cannot be updated. If you need to make changes, we recommend canceling the existing entry and creating a new one.",Lager Reservation Post skapad mot Plocklista kan inte uppdateras. Om man behöver göra ändringar rekommenderas att man anullerar befintlig post och skapar ny. , +Stock Reservation Warehouse Mismatch,Lager Reservation för Lager stämmer inte, +Stock Reservation can only be created against {0}.,Lager Reservation kan endast skapas mot {0}., +Stock Reserved Qty (in Stock UOM),Lager Reserverad Kvantitet (Lager Enhet), +Stock Transactions Settings,Lager Transaktion Inställningar, +Stock UOM Quantity,Lager Kvantitet, +Stock Unreservation,Lager Reservation Annullering, +Stock Validations,Lager Validering, +Stock and Manufacturing,Lager & Produktion, +Stock cannot be reserved in group warehouse {0}.,Lager kan inte reserveras i grupp lager {0}., +Stock cannot be reserved in the group warehouse {0}.,Lager kan inte reserveras i grupp lager {0}., +Stock not available for Item {0} in Warehouse {1}.,Lager ej tillgängligt för Artikel {0} i Lager {1}., +Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2} {3}.,Lager Kvantitet ej tillgänglig för Artikel Kod: {0} på lager {1}. Tillgänglig kvantitet {2} {3}., +Stock transactions that are older than the mentioned days cannot be modified.,Lager Transaktioner som är äldre än angiven antal dagar kan inte ändras., +Stock will be reserved on submission of Purchase Receipt created against Material Request for Sales Order.,Lager kommer att reserveras vid godkännade av Inköp Följesedel skapat mot Material Begäran för Försäljning Order., +Stock/Accounts can not be frozen as processing of backdated entries is going on. Please try again later.,Lager/Konton kan inte låsas eftersom bearbetning av bakdaterade poster pågår. Försök igen senare., +Sub Assemblies & Raw Materials,Delmonteringar & Råmaterial, +Sub Assembly Item,Delmontering Artikel, +Sub Assembly Item Code,Delmontering Artikel Kod, +Sub Assembly Items,Delmontering Artiklar, +Sub Assembly Warehouse,Delmontering Lager, +Sub Operation,Underåtgärd, +Sub Operations,Underåtgärder, +Subcontract BOM,Underleverantör Stycklista, +Subcontract Order,Underleverantör Order, +Subcontract Order Summary,Underleverantör Order Översikt, +Subcontract Return,Underleverantör Retur, +Subcontracting BOM,Underleverantör Stycklista, +Subcontracting Order,Underleverantör Order, +Subcontracting Order (Draft) will be auto-created on submission of Purchase Order.,Underleverantör Order (Utkast) kommer att skapas automatiskt vid godkännande av Inköp Order., +Subcontracting Order Item,Underleverantör Order Artikel, +Subcontracting Order Service Item,Underleverantör Order Service Artikel, +Subcontracting Order Supplied Item,Underleverantör Order Levererad Artikel, +Subcontracting Order {0} created.,Underleverantör Order {0} skapad, +Subcontracting Purchase Order,Underleverantör Inköp Order, +Subcontracting Receipt,Underleverantör Följesedel/Kvitto, +Subcontracting Receipt Item,Underleverantör Följesedel/Kvitto Artikel, +Subcontracting Receipt Supplied Item,Underleverantör Följesedel/Kvitto Levererad Artikel, +Subcontracting Settings,Underleverantör Inställningar, +Subdivision,Underavdelning, +Submit Action Failed,Godkännande Misslyckades, +Submit After Import,Godkänn efter Import, +Submit ERR Journals?,Godkänn ERR Journaler ?, +Submit Generated Invoices,Godkänn Skapade Fakturor, +Subscription for Future dates cannot be processed.,Prenumeration för framtida datum kan inte behandlas., +Succeeded,Klar, +Succeeded Entries,Klara Poster, +"Successfully changed Stock UOM, please redefine conversion factors for new UOM.","Lager Enhet ändrad, ändra konvertering faktor för ny enhet.", +Successfully imported {0},Importerade {0}, +"Successfully imported {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.","Importerade {0} post av {1}. Klicka på Exportera felaktiga rader, åtgärda fel och importera igen.", +Successfully imported {0} record.,Importerade {0} post., +"Successfully imported {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.","Importerade {0} poster av {1}. Klicka på Exportera felaktiga rader, åtgärda fel och importera igen.", +Successfully imported {0} records.,Importerade {0} poster., +Successfully linked to Customer,Länkad till Kund, +Successfully linked to Supplier,Länkad till Leverantör, +Successfully merged {0} out of {1}.,Slog samman {0} av {1}., +Successfully updated {0},Uppdaterade {0}, +"Successfully updated {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.","Uppdaterade {0} post av {1}. Klicka på Exportera felaktiga rader, åtgärda fel och importera igen.", +Successfully updated {0} record.,Uppdaterade {0} post., +"Successfully updated {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.","Uppdaterade {0} poster av {1}. Klicka på Exportera felaktiga rader, åtgärda fel och importera igen.", +Successfully updated {0} records.,Uppdaterade {0} poster., +Sum of Repair Cost and Value of Consumed Stock Items.,Summa för reparation kostnad och värde för förbrukade lager artiklar., +Supplied Item,Levererad Artikel, +Supplier Address Details,Leverantör Adress Detaljer, +Supplier Contact,Leverantör Kontakt, +Supplier Group Item,Leverantör Grupp Artikel, +Supplier Info,Leverantör Info, +Supplier Invoice,Leverantör Faktura, +Supplier Item,Leverantör Artikel, +Supplier Portal Users,Leverantör  Portal Användare, +Supplier Primary Address,Leverantör Primär Adress, +Supplier Primary Contact,Leverantör Primär Kontakt, +Supplier Warehouse mandatory for sub-contracted {0},Leverantör Lager erfodras för underleverantör {0}, +Supplies subject to the reverse charge provision,Leveranser som omfattas av omvänd betalning provision, +Sync Now,Synkronisera Nu, +Sync Started,Synkronisering Startad, +System Settings,System Inställningar, +System will automatically create the serial numbers / batch for the Finished Good on submission of work order,System kommer automatiskt att skapa serienummer/parti för färdig artikel vid godkännade av arbetsorder, +System will not check over billing since amount for Item {0} in {1} is zero,System kontrollerar inte överfakturering eftersom belopp för Artikel {0} i {1} är noll, +TCS Amount,TCS Belopp, +TCS Rate %,TDS Sats %, +TDS Amount,TDS Belopp, +TDS Payable,TDS Betalbar, +Target Asset,Tillgång, +Target Asset Location,Tillgång Plats, +Target Asset {0} cannot be cancelled,Tillgång {0} kan inte annulleras, +Target Asset {0} cannot be submitted,Tillgång {0} kan inte godkännas, +Target Asset {0} cannot be {1},Tillgång {0} kan inte bli {1}, +Target Asset {0} does not belong to company {1},Tillgång {0} tillhör inte bolag {1}, +Target Asset {0} needs to be composite asset,Tillgång {0} måste vara sammansatt tillgång, +Target Batch No,Parti Nummer, +Target Exchange Rate,Växelkurs, +Target Fieldname (Stock Ledger Entry),Fältnamn (Lager Register Post), +Target Fixed Asset Account,Tillgång Konto, +Target Has Batch No,Har Parti Nummer, +Target Has Serial No,Har Serie Nummer, +Target Incoming Rate,In Pris, +Target Is Fixed Asset,Är Tillgång, +Target Item Code,Artikel Kod, +Target Item Name,Artikel Namn, +Target Item {0} is neither a Fixed Asset nor a Stock Item,Artikel {0} är varken Tillgång eller Lager Artikel, +Target Item {0} must be a Fixed Asset item,Artikel {0} måste vara Tillgång, +Target Item {0} must be a Stock Item,Artikel {0} måste vara Lager Artikel, +Target Qty must be a positive number,Kvantitet måste vara positivt tal, +Target Serial No,Serie Nummer, +Target Warehouse Address Link,Till Lager Adress Länk, +Target Warehouse is mandatory for Decapitalization,Till Lager erfodras för Dekapitalisering, +Target Warehouse is set for some items but the customer is not an internal customer.,Till Lager angiven för vissa artiklar men kund är inte intern kund., +Task {0} depends on Task {1}. Please add Task {1} to the Tasks list.,Uppgift {0} beror på Uppgift {1}. Lägg till Uppgift {1} i Uppgift Lista., +Tax Amount,Momspliktig Belopp, +Tax Amount will be rounded on a row(items) level,Moms Belopp kommer att avrundas per Artikelrad, +Tax Masters,Moms Inställningar, +Tax Refunds provided to Tourists under the Tax Refunds for Tourists Scheme,Moms Återbäring till turister enligt momsåterbäring för turister, +Tax Settings,Moms Inställningar, +Tax Withheld Vouchers,Moms Avdrag Verifikat, +Tax Withholding,Moms Avdrag, +Tax Withholding Category {} against Company {} for Customer {} should have Cumulative Threshold value.,Moms Avdrag Kategori {} mot bolag {} för kund {} ska ha Kumulativ Tröskel Värde., +Tax Withholding Details,Moms Avdrag Kategori, +Tax Withholding Net Total,Moms Avdrag Netto Totalt, +"Tax detail table fetched from item master as a string and stored in this field. +Used for Taxes and Charges","Moms Tabell hämtad från Artikel Tabell som sträng och lagrad i detta fält. +Används för Moms och Avgifter", +Tax will be withheld only for amount exceeding the cumulative threshold,Moms kommer att dras av bara för belopp som överstiger kumulativ tröskel, +Taxes row #{0}: {1} cannot be smaller than {2},Momsrad #{0}: {1} kan inte vara lägre än {2}, +Telephony Call Type,Telefoni Typ, +Template Item Selected,Mall Artikel Vald, +Template Options,Mall Alternativ, +Template Task,Mall Upgift, +Template Warnings,Mall Varningar, +Terms & Conditions,Regler & Villkor, +Terms Template,Villkor Mall, +Territory Item,Distrikt Artikel, +Territory Wise Sales,Försäljning per Distrikt, +The Condition '{0}' is invalid,Villkor '{0}' är ogiltig, +The Document Type {0} must have a Status field to configure Service Level Agreement,Dokument Typ {0} måste ha Statusfält för att konfigurera Service Nivå Avtal, +"The GL Entries will be cancelled in the background, it can take a few minutes.","Bokföring Register Poster kommer att annulleras i bakgrunden, det kan ta några minuter.", +"The Payment Request {0} is already paid, cannot process payment twice","Betalning Begäran {0} är redan betald, kan inte behandla betalning två gånger", +"The Pick List having Stock Reservation Entries cannot be updated. If you need to make changes, we recommend canceling the existing Stock Reservation Entries before updating the Pick List.",Plocklista med Lager Reservation kan inte uppdateras. Om ändringar behöver göras rekommenderas annullering av befintlig Lager Reservation innan uppdatering av Plocklista., +The Process Loss Qty has reset as per job cards Process Loss Qty,Process Förlust Kvantitet är återställd enligt Jobbkort Process Förlust Kvantitet, +The Serial No at Row #{0}: {1} is not available in warehouse {2}.,Serie Nummer på rad #{0}: {1} är inte tillgänglig i lager {2}., +The Serial and Batch Bundle {0} is not valid for this transaction. The 'Type of Transaction' should be 'Outward' instead of 'Inward' in Serial and Batch Bundle {0},"Serie och Parti Paket {0} är inte giltigt för denna transaktion. ""Typ av Transaktion"" ska vara ""Utgående"" istället för ""Ingående"" i Serie och Parti Paket {0}", +The Work Order is mandatory for Disassembly Order,Arbetsorder erfordras för Demontering Order, +The allocated amount is greater than the outstanding amount of Payment Request {0},Tilldelad Belopp är högre än utestående belopp för Betalning Begäran {0}, +The currency of invoice {} ({}) is different from the currency of this dunning ({}).,Faktura valuta {} ({}) är annan än valuta för denna påminnelse ({})., +The default BOM for that item will be fetched by the system. You can also change the BOM.,Standard Stycklista för artikel kommer att hämtas av system. Man kan också ändra Stycklista., +The field {0} in row {1} is not set,Fält {0} i rad {1} är inte angiven, +"The following Items, having Putaway Rules, could not be accomodated:","Följande Artiklar, med Lägg undan regler, kunde inte tillgodoses:", +The following assets have failed to automatically post depreciation entries: {0},Följande tillgångar kunde inte bokföra avskrivning poster automatiskt: {0}, +The following invalid Pricing Rules are deleted:,Följande ogiltiga prissättningsregler tas bort:, +The items {0} and {1} are present in the following {2} :,Artiklar {0} och {1} finns i följande {2}:, +The operation {0} can not add multiple times,Åtgärd {0} kan inte läggas till flera gånger, +The operation {0} can not be the sub operation,Åtgärd {0} kan inte vara underåtgärd, +The original invoice should be consolidated before or along with the return invoice.,Original Faktura ska konsolideras före eller tillsammans med retur faktura., +The percentage you are allowed to pick more items in the pick list than the ordered quantity.,Procentandel artiklar du får plocka utöver artiklar i plocklista., +"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.","Procentandel man får överföra mer mot order kvantitet. Till exempel, om man har order på 100 enheter och tillåtelse är 10%, får man överföra upp till 110 enheter.", +The reserved stock will be released when you update items. Are you certain you wish to proceed?,Lager Reservation kommer att släppas när artiklar uppdaterats. Fortsätt?, +The reserved stock will be released. Are you certain you wish to proceed?,Lager Reservation kommer att släppas. Fortsätt?, +The selected {0} does not contain the selected Asset Item.,Vald {0} innehåller inte vald Tillgång Post., +"The stock for the item {0} in the {1} warehouse was negative on the {2}. You should create a positive entry {3} before the date {4} and time {5} to post the correct valuation rate. For more details, please read the documentation.","Lager för artikel {0} i {1} lager var negativt {2}. Skapa positiv post {3} före {4} och {5} för att bokföra rätt grund pris. För mer information, läs dokumentation .", +"The stock has been reserved for the following Items and Warehouses, un-reserve the same to {0} the Stock Reconciliation:

{1}","Lager är reserverad för följande Artiklar och Lager, ta bort reservation till {0} Lager Inventering :

{1}", +"The sync has started in the background, please check the {0} list for new records.",Synkronisering startad i bakgrunden. Kolla {0} lista för nya poster., +The task has been enqueued as a background job.,Uppgift är i kö som bakgrund jobb., +"The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Submitted stage",Uppgift är i kö som ett bakgrund jobb. Om det finns några problem med bearbetning i bakgrund kommer system att lägga till kommentar om fel på denna Lager Inventering och återgå till Godkänd status, +The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than allowed requested quantity {2} for Item {3},Totalt Utfärdad / Överföring Kvantitet {0} i Material Begäran {1} kan inte vara högre än tillåten begärd kvantitet {2} för artikel {3}, +The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than requested quantity {2} for Item {3},Totalt Utfärdad / Överföring Kvantitet {0} i Material Begäran {1} kan inte vara högre än begärd kvantitet {2} för artikel {3}, +"The users with this Role are allowed to create/modify a stock transaction, even though the transaction is frozen.","Användare med denna roll får skapa/ändra lager transaktion, även om transaktion är låst.", +The warehouse where you store finished Items before they are shipped.,Lager där färdiga artiklar lagras innan de levereras., +"The warehouse where you store your raw materials. Each required item can have a separate source warehouse. Group warehouse also can be selected as source warehouse. On submission of the Work Order, the raw materials will be reserved in these warehouses for production usage.",Lager där råmaterial lagras. Varje erfodrad artikel kan ha separat från lager. Grupp lager kan också väljas som från lager. Vid godkännade av arbetsorder kommer råmaterial att reserveras i dessa lager för produktion., +The warehouse where your Items will be transferred when you begin production. Group Warehouse can also be selected as a Work in Progress warehouse.,Lager där artiklar kommer att överföras när produktion påbörjas. Grupp Lager kan också väljas som Arbete Pågår lager., +The {0} {1} is used to calculate the valuation cost for the finished good {2}.,{0} {1} används för att beräkna grund kostnad för färdig artikel {2}., +There are ledger entries against this account. Changing {0} to non-{1} in live system will cause incorrect output in 'Accounts {2}' report,"Det finns bokföring register aposter mot detta konto. Om du ändrar {0} till icke-{1} i system kommer det att orsaka felaktig utdata i ""Konton {2}"" rapporten ", +There are no Failed transactions,Det finns inga misslyckade transaktioner, +There are no active Fiscal Years for which Demo Data can be generated.,Det finns inga aktiva bokföringsår för vilka demo data kan genereras., +There are no slots available on this date,Det finns inga lediga tider för detta datum, +There are only {0} asset created or linked to {1}. Please create or link {2} Assets with respective document.,Det finns bara {0} tillgångar skapade eller länkade till {1}. Skapa eller länka {2} Tillgångar med respektive dokument., +"There are two options to maintain valuation of stock. FIFO (first in - first out) and Moving Average. To understand this topic in detail please visit
Item Valuation, FIFO and Moving Average.","Det finns två alternativ för att upprätthålla Lager värdering. FIFO (först in - först ut) och Medelvärde. För att förstå detta ämne i detalj besök Artikelvärdering, FIFO och Medelvärde.", +There aren't any item variants for the selected item,Det finns inga artikelvarianter för vald artikel, +There is already a valid Lower Deduction Certificate {0} for Supplier {1} against category {2} for this time period.,Det finns redan giltigt Lägre Avdrag Certifikat {0} för Leverantör {1} mot kategori {2} för denna tidsperiod., +There is already an active Subcontracting BOM {0} for the Finished Good {1}.,Det finns redan aktiv Underleverantör Stycklista {0} för färdig artikel {1}., +There must be atleast 1 Finished Good in this Stock Entry,Det måste finnas minst en färdig artikel i denna Lager Post, +There was an error creating Bank Account while linking with Plaid.,Det uppstod fel när Bank Konto skulle skapas vid länkning med Plaid., +There was an error syncing transactions.,Det uppstod fel med synkronisering av transaktioner., +There was an error updating Bank Account {} while linking with Plaid.,Det uppstod fel när Bank Konto {} skulle uppdateras vid länkning med Plaid., +There was an issue connecting to Plaid's authentication server. Check browser console for more information,Det uppstod fel vid anslutning till Plaid autentisering server. Kontrollera webbläsare konsol för mer information, +There were issues unlinking payment entry {0}.,Det uppstod fel med borttagning av länk till Betalning Post {0}., +This Account has '0' balance in either Base Currency or Account Currency,"Konto har ""0"" Saldo i antingen Standard Valuta eller Konto Valuta", +This field is used to set the 'Customer'.,Detta fält används för att ange 'Kund'., +This filter will be applied to Journal Entry.,Detta filter kommer att tillämpas på Journal Post, +This is a Template BOM and will be used to make the work order for {0} of the item {1},Detta är Stycklista Mall och kommer att användas för att skapa arbetsorder för {0} av artikel {1}, +This is considered dangerous from accounting point of view.,Detta anses vara farligt ur bokföring synpunkt., +"This is enabled by default. If you want to plan materials for sub-assemblies of the Item you're manufacturing leave this enabled. If you plan and manufacture the sub-assemblies separately, you can disable this checkbox.","Detta är aktiverat som standard. Planeras material för underenheter för artikel som produceras, lämna detta aktiverat. Planeras och produceras underenheterna separat kan den inaktiveras.", +"This is for raw material Items that'll be used to create finished goods. If the Item is an additional service like 'washing' that'll be used in the BOM, keep this unchecked.","Detta är för råmaterial artiklar som kommer att användas för att skapa färdiga artiklar. Om artikel är tillägg service som ""tvätt"" som kommer att användas i stycklista, låt den vara inaktiverad", +This item filter has already been applied for the {0},Detta artikel filter har redan tillämpats för {0}, +This option can be checked to edit the 'Posting Date' and 'Posting Time' fields.,Detta alternativ kan väljas för att redigera fält 'Post Datum' och 'Post Tid'., +This schedule was created when Asset {0} was adjusted through Asset Value Adjustment {1}.,Detta schema skapades när Tillgång {0} justerades genom Tillgång Värde Justering {1}., +This schedule was created when Asset {0} was consumed through Asset Capitalization {1}.,Detta schema skapades när Tillgång {0} förbrukades genom Tillgång Kapitalisering {1}., +This schedule was created when Asset {0} was repaired through Asset Repair {1}.,Detta schema skapades när Tillgång {0} reparerades genom Tillgång Reparation {1}., +This schedule was created when Asset {0} was restored on Asset Capitalization {1}'s cancellation.,Detta schema skapades när Tillgång {0} återställdes vid annullering av Tillgång Kapitalisering {1}., +This schedule was created when Asset {0} was restored.,Detta schema skapades när Tillgång {0} återställdes., +This schedule was created when Asset {0} was returned through Sales Invoice {1}.,Detta schema skapades när Tillgång {0} returnerades via Försäljning Faktura {1}., +This schedule was created when Asset {0} was scrapped.,Detta schema skapades när Tillgång {0} skrevs av., +This schedule was created when Asset {0} was sold through Sales Invoice {1}.,Detta schema skapades när Tillgång {0} såldes via Försäljning Faktura {1}., +This schedule was created when Asset {0} was updated after being split into new Asset {1}.,Detta schema skapades när Tillgång {0} uppdaterades efter att ha delats upp i ny Tillgång {1}., +This schedule was created when Asset {0}'s Asset Repair {1} was cancelled.,Detta schema skapades när Tillgång Reparation {0} Tillgång Reparation {1} annullerades., +This schedule was created when Asset {0}'s Asset Value Adjustment {1} was cancelled.,Detta schema skapades när Tillgång {0} Tillgång Värde Justering {1} annullerades., +This schedule was created when Asset {0}'s shifts were adjusted through Asset Shift Allocation {1}.,Detta schema skapades när Tillgång {0} förskjutning justerades genom Tillgång Förskjutning Tilldelning {1}., +This schedule was created when new Asset {0} was split from Asset {1}.,Detta schema skapades när ny Tillgång {0} delades från Tillgång {1}., +"This table is used to set details about the 'Item', 'Qty', 'Basic Rate', etc.","Denna tabell används för att ange detaljer om 'Artikel', 'Kvantitet', 'Bas Pris', etc.", +This {} will be treated as material transfer.,Denna {} kommer att behandlas som material överföring., +Threshold for Suggestion (In Percentage),Tröskel för Förslag (%), +Time Taken to Deliver,Tid som tagits att Leverera, +Time in mins,Tid i minuter, +Time in mins.,Tid i minuter, +Time slot is not available,Tid är inte tillgänglig, +To Date is mandatory,Till Datum Erfordras, +To Delivery Date,Till Leverans Datum, +To Doctype,Till DocType, +To Due Date,Till Förfallo Datum, +To Payment Date,Till Betalning Datum, +To Reference Date,Till Referens Datum, +To add Operations tick the 'With Operations' checkbox.,Att lägga till Åtgärder kryssa i rutan 'Med Åtgärder'., +To add subcontracted Item's raw materials if include exploded items is disabled.,Att lägga till Underleverantör Artikel råmaterial om Inkludera Utvidgade Artiklar är inaktiverad., +To apply condition on parent field use parent.field_name and to apply condition on child table use doc.field_name. Here field_name could be based on the actual column name of the respective field.,"Att tillämpa villkor på överordnad fält, använd parent.field_name och för att tillämpa villkor på underordnad tabell använd doc.field_name. Här kan fältnamn baseras på verklig kolumn namn för respektive fält.", +To be Delivered to Customer,Levereras till Kund, +To cancel a {} you need to cancel the POS Closing Entry {}.,Att annullera {} måste du annullera Kassa Stängning Post {}., +"To enable Capital Work in Progress Accounting,",Att aktivera Pågående Kapitalarbete Bokföring, +To include non-stock items in the material request planning. i.e. Items for which 'Maintain Stock' checkbox is unticked.,Att inkludera ej lagerartiklar i material begäran planering. dvs Artiklar för vilka ruta 'Lager Hantera' är inaktiverad., +To submit the invoice without purchase order please set {0} as {1} in {2},"Att godkänna faktura utan inköp order, ange {0} som {1} i {2}", +To submit the invoice without purchase receipt please set {0} as {1} in {2},Att godkänna faktura utan inköp följesedel ange {0} som {1} i {2}, +"To use a different finance book, please uncheck 'Include Default FB Assets'","Att använda annan finans register, inaktivera ""Inkludera Standard Finans Register Tillgångar""", +"To use a different finance book, please uncheck 'Include Default FB Entries'","Att använda annan finans register, inaktivera ""Inkludera Standard Finans Register Tillgångar""", +Total Active Items,Totalt Aktiva Artiklar, +Total Allocations,Totala Tilldelningar, +Total Asset,Totalt Tillgång, +Total Asset Cost,Totalt Tillgång Kostnad, +Total Billing Hours,Totalt Fakturerbara Timmar, +Total Contribution Amount Against Invoices: {0},Totalt Bidrag Belopp Mot Fakturor: {0}, +Total Contribution Amount Against Orders: {0},Totalt Bidrag Belopp Mot Ordrar: {0}, +Total Equity,Totalt Eget Kapital, +Total Incoming Bills,Inkommande Fakturor, +Total Incoming Payment,Inkommande Betalningar, +Total Incoming Value (Receipt),Totalt Ingående Värde (Faktura), +Total Interest,Totalt Ränta, +Total Issues,Totalt Frågor, +Total Items,Totalt Artiklar, +Total Liability,Totalt Skuld, +Total Number of Booked Depreciations ,Totalt Antal Bokförda Avskrivningar , +Total Operation Time,Totalt Drift Tid, +Total Other Charges,Totalt Övriga Avgifter, +Total Outgoing Bills,Utgående Fakturor, +Total Outgoing Payment,Utgående Betalningar, +Total Outgoing Value (Consumption),Utgående Värde (Förbrukning), +Total Picked Quantity {0} is more than ordered qty {1}. You can set the Over Picking Allowance in Stock Settings.,Totalt plockad kvantitet {0} är mer än order kvantitet {1}. Du kan ange överplock tillåtelse i Lager Inställningar., +Total Purchase Amount,Totalt Inköp Belopp, +Total Purchase Cost has been updated,Totalt Inköp Kostnad uppdaterad, +Total Repair Cost,Totalt Reparation Kostnad, +Total Reposting Count,Totalt Ompostering Antal, +Total Sales Amount,Totalt Försäljning Belopp, +Total Stock Value,Totalt Lager Värde, +Total Supplied Qty,Totalt Levererad Kvantitet, +Total Time (in Mins),Totalt Tid i Minuter, +Total Value,Totalt Värde, +Total Value Difference (Incoming - Outgoing),Värde Differens (Inkommande - Utgående), +Total Views,Totalt Visningar, +Total Warehouses,Totalt Antal Lager, +Total percentage against cost centers should be 100,Totalt procentsats mot resultat enhet ska vara 100%, +Tracking Status,Spårning Status, +Tracking Status Info,Spårning Status Info, +Tracking URL,Spårning URL, +Transaction Deletion Document: {0} is running for this Company. {1},Transaktion Borttagning Dokument: {0} körs redan för {1}, +Transaction Deletion Record,Transaktion Borttagning Post, +Transaction Deletion Record Details,Transaktion Borttagning Post Detaljer, +Transaction Deletion Record Item,Transaktion Borttagning Post Artikel, +Transaction Exchange Rate,Transaktion Växelkurs, +Transaction Settings,Transaktion Inställningar, +Transaction currency: {0} cannot be different from Bank Account({1}) currency: {2},Transaktion valuta: {0} kan inte skilja sig från Bank Konto ({1}) valuta: {2}, +Transactions against the Company already exist! Chart of Accounts can only be imported for a Company with no transactions.,Transaktioner mot bolag finns redan! Kontoplan kan endast importeras för bolag utan transaktioner., +Transfer Asset,Överför Tillgång, +Transfer From Warehouses,Överföring Från Lager, +Transferring cannot be done to an Employee. Please enter location where Asset {0} has to be transferred,Överföring kan inte skapas för Personal. Ange plats där Tillgång {0} ska överföras, +Transit Entry,Transit Post, +Truncates 'Remarks' column to set character length,"Trunkerar kolumn ""Anmärkningar"" för att ange teckenlängd", +Type Of Call,Typ av Samtal, +Type of Transaction,Typ av Transaktion, +UAE VAT Account,UAE VAT Konto, +UAE VAT Accounts,UAE VAT Konton, +UAE VAT Settings,UAE VAT Inställningar, +UOM conversion factor required for UOM: {0} in Item: {1},Enhet Konvertering Faktor erfodras för Enhet: {0} för Artikel: {1}, +UnReconcile,Ångra, +Unable to find the time slot in the next {0} days for the operation {1}. Please increase the 'Capacity Planning For (Days)' in the {2}.,"Kunde inte att hitta tider under de kommande {0} dagarna för åtgärd {1}. Öka ""Kapacitet Planering för (Dagar)"" i {2}.", +Unable to find variable:,Kan inte hitta variabel:, +Unassigned Qty,Ej Tilldelat Kvantitet, +"Under Working Hours table, you can add start and end times for a Workstation. For example, a Workstation may be active from 9 am to 1 pm, then 2 pm to 5 pm. You can also specify the working hours based on shifts. While scheduling a Work Order, the system will check for the availability of the Workstation based on the working hours specified.","Under Arbetstid tabell kan man lägga till start och slut tider för arbetsstation. Till exempel kan arbetsstation vara aktiv från 9.00 till 12.00, sedan 1300 till 17.00. Du kan även ange arbetstid utifrån skift. Under schemaläggning av arbetsorder kommer system att kontrollera tillgänglighet för arbetsstation baserat på angiven arbetstid.", +Unit of Measure (UOM),Enhet, +Unlinked,Bortkopplad, +Unqualified,Okvalificerad, +Unrealized Profit / Loss Account,Orealiserad Resultat Konto, +Unrealized Profit / Loss account for intra-company transfers,Orealiserad Resultat konto för koncern överföringar, +Unrealized Profit/Loss account for intra-company transfers,Orealiserad Resultat konto för koncern överföringar, +Unreconcile Payment,Ångra Betalning Avstämning, +Unreconcile Payment Entries,Ångra Betalning Avstämning Post, +Unreconcile Transaction,Ångra Transaktion, +Unreconciled Amount,Ångrad Betalning Avstämning Belopp, +Unreconciled Entries,Ångrad Betalning Avstämning Post, +Unreserve,Ångra Reservation, +Unreserve Stock,Ångra Lager Reservation, +Unreserving Stock...,Ångrar Lager Reservation ..., +Unset Matched Payment Request,Ångra Avstämd Betalning Begäran, +Up,Upp, +Update Billed Amount in Delivery Note,Uppdatera Fakturerad Belopp i Försäljning Följesedel, +Update Billed Amount in Purchase Order,Uppdatera Fakturerad Belopp i Inköp Order, +Update Billed Amount in Purchase Receipt,Uppdatera Fakturerad Belopp i Inköp Följesedel, +Update Existing Price List Rate,Uppdatera Befintlig Prislista Pris, +Update Existing Records,Uppdatera Befintliga Poster, +Update Outstanding for Self,Uppdatera Utestående för sig själv, +Update Rate as per Last Purchase,Uppdatera Pris per Senaste Inköp, +Update Total Purchase Cost,Uppdatera Total Inköp Kostnad, +Update frequency of Project,Projekt Uppdatering Intervall, +Update stock must be enabled for the purchase invoice {0},Uppdatera Lager måste vara aktiverat för Inköp Faktura {0}, +Updated via 'Time Log' (In Minutes),Uppdaterad via 'Tid Logg' (i Minuter), +Updating Work Order status,Uppdaterar Arbetsorder status, +"Updating {0} of {1}, {2}","Uppdaterar {0} av {1}, {2}", +Upload Bank Statement,Importera Bank Avstämning, +Use 'Repost in background' button to trigger background job. Job can only be triggered when document is in Queued or Failed status.,"Använd knapp ""Posta om i Backgrund"" för att utlösa bakgrund jobb. Jobb kan bara utlösas när dokument har status 'I Kö' eller 'Misslyckad'.", +Use Batch-wise Valuation,Använd Värdering per Parti, +Use Company Default Round Off Cost Center,Använd Bolag Standard Avrundning Resultat Enhet, +Use Company default Cost Center for Round off,Använd Bolag standard Resultat Enhet för Avrundning, +Use HTTP Protocol,Använd HTTP Protokoll, +Use Item based reposting,Använd Artikel baserad Ompostering , +Use Serial / Batch Fields,Använd Serie / Parti Nummer Fält, +Use Serial No / Batch Fields,Använd Serie / Parti Nummer Fält, +Use Transaction Date Exchange Rate,Använd Transaktion Datum Växelkurs, +User {0}: Removed Employee Self Service role as there is no mapped employee.,Användare {0}: Borttagen Personal Självbetjäning roll eftersom det inte finns någon mappad personal., +User {0}: Removed Employee role as there is no mapped employee.,Användare {0}: Borttagen Personal roll eftersom det inte finns mappad personal., +Users can enable the checkbox If they want to adjust the incoming rate (set using purchase receipt) based on the purchase invoice rate.,Användare kan kryssa i rut Om de vill justera inköp pris (anges med inköp följesedel) baserat på inköp faktura pris., +Users with this role are allowed to over bill above the allowance percentage,Användare med denna roll tillåts att överfakturera över tillåten procentsats, +Users with this role are allowed to over deliver/receive against orders above the allowance percentage,Användare med denna roll tillåts att överleverera/ta emot ordrar över tillåten procentsats, +Using negative stock disables FIFO/Moving average valuation when inventory is negative.,Användning av negativ lager inaktiverar FIFO/MA värdering sätt när lager värde är negativ., +VAT Accounts,Moms Konton, +VAT Amount (AED),Moms Belopp (AED), +VAT Audit Report,Moms Revision Rapport, +VAT on Expenses and All Other Inputs,Moms på Utgifter och Alla Andra intäkter, +VAT on Sales and All Other Outputs,Moms på Försäljning och Alla Andra utgifter, +Valid From must be after {0} as last GL Entry against the cost center {1} posted on this date,Giltig Från Datum måste vara efter {0} eftersom senaste Bokföring Register Post mot resultat enhet {1} postad detta datum, +Validate Components Quantities Per BOM,Validera Komponent Kvantiteter per Stycklista, +Validate Negative Stock,Validera Negativ Lager, +Validate Pricing Rule,Validera Prissättning Regel, +Validate Stock on Save,Validera Lager på Spara, +Valuation Field Type,Värdering Fält Typ, +Valuation Rate (In / Out),Grund Pris (In/Ut), +Valuation rate for customer provided items has been set to zero.,Grund Pris för Kund Försedda Artiklar angavs till noll., +Valuation rate for the item as per Sales Invoice (Only for Internal Transfers),Grund Pris för artikel enligt Försäljning Faktura (endast för Interna Överföringar), +Value Based Inspection,Värde Baserad Kontroll, +Value Change,Värde Förändring, +Value Details,Värde Detaljer, +Value of Goods,Gods Värde, +Value of goods cannot be 0,Godsvärde kan inte vara 0, +Verification failed please check the link,"Verifiering misslyckades, kontrollera länk", +Via Landed Cost Voucher,Genom Landad Kostnad Verifikat, +View BOM Update Log,Visa Stycklista Uppdatering Logg, +View Exchange Gain/Loss Journals,Visa Växelkurs Resultat Journaler, +View General Ledger,Visa Bokföring Register, +View Ledgers,Visa Register, +Visits,Besök, +Voice Call Settings,Röst Samtal Inställningar, +Voucher,Verifikat, +Voucher Name,Verifikat Namn, +Voucher No is mandatory,Verifikat Nummer Erfodras, +Voucher Qty,Verifikat Kvantitet, +Voucher Subtype,Verifikat Undertyp, +Voucher {0} is over-allocated by {1},Verifikat {0} är övertilldelad av {1}, +Voucher {0} value is broken: {1},Verifikat {0} värde bruten: {1}, +Voucher-wise Balance,Saldo per Verifikat, +"WARNING: Exotel app has been separated from ERPNext, please install the app to continue using Exotel integration.","OBS: Exotel app har separerats från System, installera app för att fortsätta använda Exotelintegration.", +WIP Composite Asset,Pågående Arbete Sammansatt Tillgång, +Waiting for payment...,Väntar på betalning..., +Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} {2}.,Lager Kapacitet för Artikel '{0}' måste vara högre än befintlig lager nivå på {1} {2}. , +Warehouse Details,Lager Detaljer, +Warehouse Disabled?,Lager Inaktiverad?, +Warehouse Settings,Lager Inställningar, +Warehouse Wise Stock Balance,Lager Saldo per Lager, +Warehouse wise Stock Value,Lager Värde per Lager, +Warehouse {0} does not belong to Company {1}.,Lager {0} tillhör inte Bolag {1}., +"Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}.",Lager {0} är inte länkad till något konto. Ange konto i lager post eller ange standard konto för lager i bolag {1}., +Warehouse's Stock Value has already been booked in the following accounts:,Lagrets Lager Värde är redan bokförd på följande konton:, +Warning - Row {0}: Billing Hours are more than Actual Hours,Varning - Rad # {0}: Fakturerbara timmar är fler än verkliga timmar, +Warning on Negative Stock,Varna vid Negativt Lager, +Warning!,Varning!, +Watch Video,Visa Video, +"We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, uncheck '{2}' checkbox.

Or you can use {3} tool to reconcile against {1} later.","Vi kan se att {0} skapades mot {1}. Om du vill att {1}s utestående ska uppdateras, avmarkera '{2}'kryssruta .

Eller så kan du använda {3} verktyg för att stämma av mot {1} senare.", +Website Script,Webbplats Skript, +Website Theme,Webbplats Tema, +Week {0} {1},Vecka {0} {1}, +Weekly Time to send,Veckotid att skicka, +Weight (kg),Vikt (kg), +"When a parent warehouse is chosen, the system conducts stock checks against the associated child warehouses",Näröverordnat lager valts utför system lagerkontroller mot tillhörande underordnade lager, +"When creating an Item, entering a value for this field will automatically create an Item Price at the backend.","Vid skapande av Artikel, om man anger värde för detta fält, skapas automatiskt artikelpris i bakgrund.", +"While making Purchase Invoice from Purchase Order, use Exchange Rate on Invoice's transaction date rather than inheriting it from Purchase Order. Only applies for Purchase Invoice.","Vid skapande av Inköp Faktura från Inköp Order, använd Inköp Faktura transaktion datum för växelkurs istället för att ärva den från Inköp Order. Gäller endast Inköp Faktura.", +Width (cm),Bredd (cm), +With Period Closing Entry For Opening Balances,Med PeriodStängning Post för Öppning Saldon, +Withdrawal,Uttag, +Work Order / Subcontract PO,Arbetsorder / Underleverantör Inköp Order, +Work Order Consumed Materials,Arbetsorder Förbrukat Material, +Workflow,Arbetsflöde, +Workflow Action,Arbetsflöde Åtgärd, +Workflow State,Arbetsflöde Tillstånd, +Workstation Dashboard,Arbetsplats Översikt Panel, +Workstation Status,Arbetsplats Status, +Workstation Type,Arbetsplats Typ, +Workstations,Arbetsplatser, +Write Off Limit,Avskrivning Gräns, +Wrong Company,Fel Bolag, +Wrong Template,Fel Mall, +You are not authorized to make/edit Stock Transactions for Item {0} under warehouse {1} before this time.,Du är inte behörig att skapa/redigera lager transaktioner för artikel {0} under lager {1} före denna tidpunkt., +You are picking more than required quantity for the item {0}. Check if there is any other pick list created for the sales order {1}.,Du väljer mer än vad som krävs för artikel {0}. Kontrollera om det finns någon annan plocklista skapad för försäljning order {1}., +You can add the original invoice {} manually to proceed.,Lägg till original faktura {} manuellt för att fortsätta., +"You can set it as a machine name or operation type. For example, stiching machine 12",Du kan ange den som maskin namn eller åtgärd typ. Till exempel sy maskin 12, +You can't make any changes to Job Card since Work Order is closed.,Du kan inte göra några ändringar i Jobbkort eftersom Arbetsorder är stängd., +You can't redeem Loyalty Points having more value than the Rounded Total.,Det går inte att lösa in Lojalitet Poäng som har mer värde än Avrundad Belopp., +You cannot change the rate if BOM is mentioned against any Item.,Du kan inte ändra pris om Stycklista är angiven mot någon artikel., +You cannot create a {0} within the closed Accounting Period {1},Du kan inte skapa {0} inom stängd bokföring period {1}, +You cannot create/amend any accounting entries till this date.,Du kan inte skapa/ändra några bokföring poster fram till detta datum., +You cannot repost item valuation before {},Du kan inte lägga om artikel värdering före {}, +You have entered a duplicate Delivery Note on Row,Du har angett dubblett Försäljning Följesedel på Rad, +You haven't created a {0} yet,Ingen {0} skapad än, +You need to cancel POS Closing Entry {} to be able to cancel this document.,Annullera Kassa Stängning Post {} för att annullera detta dokument., +Your Name (required),Ditt Namn, +Your email has been verified and your appointment has been scheduled,Din E-post är verifierad och ditt möte är bokad, +Zero Balance,Noll Saldo, +Zero Rated,Noll Pris, +Zero quantity,Noll Kvantitet, +`Allow Negative rates for Items`,"""Tillåt Negativa Priser för Artiklar"".", +as a percentage of finished item quantity,som procentsats av färdig artikel kvantitet, +at,kl., +dated {0},daterad {0}, +description,Beskrivning, +discount applied,Rabatt Tillämpad, +fieldname,Fält Namn , +is already,är redan, +must be between 0 and 100,måste vara mellan 0 och 100, +or its descendants,eller dess underordnad, +out of 5,av 5 möjliga, +payments app is not installed. Please install it from {0} or {1},payment app är inte installerad. Installera det från {0} eller {1}, +payments app is not installed. Please install it from {} or {},payment app är inte installerad. Installera det från {0} eller {1}, +performing either one below:,utför någon av dem nedan:, +product bundle item row's name in sales order. Also indicates that picked item is to be used for a product bundle,Artikel paket artikel rad namn i försäljning order. Indikerar också att plockad artikel ska användas för artikel paket, +quotation_item,Försäljning Offert Artikel, +ratings,Bedömningar, +subscription is already cancelled.,prenumeration är redan annullerad., +to unallocate the amount of this Return Invoice before cancelling it.,att ta bort belopp för denna Retur Faktura innan annullering., +variance,avvikelse, +via BOM Update Tool,via Ersätt Stycklista Verktyg, +will be,kommer vara, +{0} {1} has submitted Assets. Remove Item {2} from table to continue.,{0} {1} har godkänt tillgångar. Ta bort Artikel {2} från tabell för att fortsätta., +{0} Account not found against Customer {1}.,{0} Konto hittades inte mot Kund {1}., +{0} Account: {1} ({2}) must be in either customer billing currency: {3} or Company default currency: {4},{0} Konto: {1} ({2}) måste vara antingen i kundens faktura valuta: {3} eller bolag standard valuta: {4}, +{0} Budget for Account {1} against {2} {3} is {4}. It {5} exceed by {6},{0} Budget för Konto {1} mot {2} {3} är {4}. Det {5} överstiger med {6}, +{0} Transaction(s) Reconciled,{0} Transaktion(er) Avstämda, +{0} account is not of type {1},{0} konto är inte av typ {1}, +{0} account not found while submitting purchase receipt,{0} konto hittades inte när vid godkänande av Inköp Följesedel, +{0} and {1},{0} och {1}, +{0} cannot be used as a Main Cost Center because it has been used as child in Cost Center Allocation {1},{0} kan inte användas som Överordnad Resultat Enhet eftersom det har använts som underordnad i Resultat Enhet Tilldelning {1}, +{0} cannot be zero,{0} kan inte vara noll, +{0} currency must be same as company's default currency. Please select another account.,{0} valuta måste vara samma som bolag standard valuta. Välj ett annat konto., +{0} entered twice {1} in Item Taxes,{0} angiven två gånger {1} under Artikel Moms, +{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section,{0} har Betalning Villkor baserad tilldelning aktiverad. Välj Betalning Villkor för Rad #{1} i Betalning Referenser, +{0} is a mandatory Accounting Dimension.
Please set a value for {0} in Accounting Dimensions section.,{0} är erfordrad Bokföring Dimension.
Ange värde för {0} Bokföring Dimensioner., +{0} is added multiple times on rows: {1},{0} läggs till flera gånger på rader: {1}, +{0} is already running for {1}, {0} körs redan för {1}, +{0} is mandatory for account {1},{0} är erfodrad för konto {1}, +{0} is not running. Cannot trigger events for this Document,{0} körs inte. Kan inte utlösa händelser för detta Dokument, +{0} qty of Item {1} is being received into Warehouse {2} with capacity {3}.,{0} kvantitet av artikel {1} tas emot i Lager {2} med kapacitet {3}., +"{0} units are reserved for Item {1} in Warehouse {2}, please un-reserve the same to {3} the Stock Reconciliation.","{0} enheter är reserverade för Artikel {1} i Lager {2}, ta bort reservation för {3} Lager Inventering.", +{0} units of Item {1} is not available in any of the warehouses.,{0} enheter av Artikel {1} är inte tillgängliga på Lager., +{0} units of Item {1} is picked in another Pick List.,{0} enheter av Artikel {1} är vald i en annan Plocklista., +"{0} units of {1} are required in {2}{3}, on {4} {5} for {6} to complete the transaction.","{0} enheter av {1} erfordras i {2}{3}, den {4} {5} för att {6} ska kunna slutföra transaktion.", +{0} units of {1} needed in {2} on {3} {4} to complete this transaction.,{0} enheter av {1} behövs i {2} den {3} {4} för att slutföra denna transaktion., +{0} will be given as discount.,{0} kommer att ges som rabatt., +{0} {1} Manually,{0} {1} Manuellt, +{0} {1} Partially Reconciled,{0} {1} Delvis Avstämd, +"{0} {1} cannot be updated. If you need to make changes, we recommend canceling the existing entry and creating a new one.",{0} {1} kan inte uppdateras. Om du behöver göra ändringar rekommenderar vi att du annullerar befintlig post och skapar ny., +{0} {1} has already been fully paid.,{0} {1} är redan betalad till fullo., +{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts.,"{0} {1} är redan delvis betald. Använd knapp ""Hämta Utestående Faktura"" eller ""Hämta Utestående Ordrar"" knapp för att hämta senaste utestående belopp.", +{0} {1} is allocated twice in this Bank Transaction,{0} {1} är tilldelad två gånger i denna Bank Transaktion, +{0} {1} is not in any active Fiscal Year,{0} {1} är inte under något aktivt Bokföringsår, +{0} {1} is on hold,{0} {1} är parkerad, +{0} {1} not allowed to be reposted. Modify {2} to enable reposting.,{0} {1} får inte läggas upp igen. Ändra {2} för att aktivera omläggning., +{0} {1} via CSV File,{0} {1} via CSV fil, +{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions,{0} {1}: Konto {2} är ett grupp konto och grupp konton kan inte användas i transaktioner, +{0} {1}: Cost Center is required for 'Profit and Loss' account {2}.,"{0} {1}: Resultat Enhet erfordras för ""Resultat"" konto {2}.", +{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions,{0} {1}: Resultat Enhet {2} är grupp resultat enhet och grupp resultat enhet kan inte användas i transaktioner, +{0}'s {1} cannot be after {2}'s Expected End Date.,{0}s {1} kan inte vara efter förväntad slut datum för {2}, +{item_name}'s Sample Size ({sample_size}) cannot be greater than the Accepted Quantity ({accepted_quantity}),{item_name} Prov Kvantitet ({sample_size}) kan inte vara högre än accepterad kvantitete ({accepted_quantity}), +{} Available,{} Tillgängliga, +{} To Deliver,{} Att Leverera, +{} To Receive,{} Att Ta Emot, +{} Assigned,{} Tilldelade, +{} Available,{} Tillgängliga, +{} Open,{} Öppen, +{} Pending,{}Pågående, +{} To Bill,{} Att Fakturera, +{} is a child company.,{} är dotter bolag., +{} {} is already linked with another {},{} {} är redan länkad till annan {}, +{} {} is already linked with {} {},{} {} är redan länkad till {} {}, diff --git a/erpnext/utilities/bulk_transaction.py b/erpnext/utilities/bulk_transaction.py index 4319fa7d6ea0..7ba687941c92 100644 --- a/erpnext/utilities/bulk_transaction.py +++ b/erpnext/utilities/bulk_transaction.py @@ -137,6 +137,11 @@ def task(doc_name, from_doctype, to_doctype): }, "Purchase Receipt": {"Purchase Invoice": purchase_receipt.make_purchase_invoice}, } + + hooks = frappe.get_hooks("bulk_transaction_task_mapper") + for hook in hooks: + mapper.update(frappe.get_attr(hook)()) + frappe.flags.bulk_transaction = True if to_doctype in ["Payment Entry"]: obj = mapper[from_doctype][to_doctype](from_doctype, doc_name) diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 6fab5380c382..2e4bdac6aab4 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -257,11 +257,11 @@ def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None): if isinstance(qty_fields, str): qty_fields = [qty_fields] - distinct_uoms = list(set(d.get(uom_field) for d in doc.get_all_children())) - integer_uoms = list( - filter( - lambda uom: frappe.db.get_value("UOM", uom, "must_be_whole_number", cache=True) or None, - distinct_uoms, + distinct_uoms = tuple(set(uom for uom in (d.get(uom_field) for d in doc.get_all_children()) if uom)) + integer_uoms = set( + d[0] + for d in frappe.db.get_values( + "UOM", (("name", "in", distinct_uoms), ("must_be_whole_number", "=", 1)), cache=True ) ) diff --git a/pyproject.toml b/pyproject.toml index aaac05d7ed08..d891b186d891 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ requires = ["flit_core >=3.4,<4"] build-backend = "flit_core.buildapi" [tool.bench.frappe-dependencies] -frappe = ">=15.10.0,<16.0.0" +frappe = ">=15.40.4,<16.0.0" [tool.ruff] line-length = 110