diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml index c62622eecec..5ec90df78ca 100644 --- a/.github/workflows/server-tests.yml +++ b/.github/workflows/server-tests.yml @@ -25,7 +25,7 @@ jobs: fail-fast: false matrix: - container: [1, 2, 3] + container: [1, 2] name: Python Unit Tests diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml deleted file mode 100644 index 9f142bd2c2f..00000000000 --- a/.github/workflows/ui-tests.yml +++ /dev/null @@ -1,117 +0,0 @@ -name: UI - -on: - pull_request: - paths-ignore: - - '**.md' - workflow_dispatch: - -concurrency: - group: ui-v13-${{ github.event.number }} - cancel-in-progress: true - -jobs: - test: - runs-on: ubuntu-18.04 - timeout-minutes: 60 - - strategy: - fail-fast: false - - name: UI Tests (Cypress) - - services: - mysql: - image: mariadb:10.3 - env: - MYSQL_ALLOW_EMPTY_PASSWORD: YES - ports: - - 3306:3306 - options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 - - steps: - - name: Clone - uses: actions/checkout@v2 - - - name: Setup Python - uses: actions/setup-python@v2 - with: - python-version: 3.7 - - - uses: actions/setup-node@v2 - with: - node-version: 14 - check-latest: true - - - name: Add to Hosts - run: | - echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts - - - name: Cache pip - uses: actions/cache@v2 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} - restore-keys: | - ${{ runner.os }}-pip- - ${{ runner.os }}- - - - name: Cache node modules - uses: actions/cache@v2 - env: - cache-name: cache-node-modules - with: - path: ~/.npm - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- - - - name: Get yarn cache directory path - id: yarn-cache-dir-path - run: echo "::set-output name=dir::$(yarn cache dir)" - - - uses: actions/cache@v2 - id: yarn-cache - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - - name: Cache cypress binary - uses: actions/cache@v2 - with: - path: ~/.cache - key: ${{ runner.os }}-cypress- - restore-keys: | - ${{ runner.os }}-cypress- - ${{ runner.os }}- - - - name: Install - run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh - env: - DB: mariadb - TYPE: ui - - - name: Site Setup - run: cd ~/frappe-bench/ && bench --site test_site execute erpnext.setup.utils.before_tests - - - name: cypress pre-requisites - run: cd ~/frappe-bench/apps/frappe && yarn add cypress-file-upload@^5 @testing-library/cypress@^8 --no-lockfile - - - - name: Build Assets - run: cd ~/frappe-bench/ && bench build - env: - CI: Yes - - - name: UI Tests - run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests erpnext --headless - env: - CYPRESS_RECORD_KEY: 60a8e3bf-08f5-45b1-9269-2b207d7d30cd - - - name: Show bench console if tests failed - if: ${{ failure() }} - run: cat ~/frappe-bench/bench_run_logs.txt diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 3798b0fbdf8..5c0d3265680 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -149,22 +149,6 @@ frappe.ui.form.on("Journal Entry", { } }); } - else if(frm.doc.voucher_type=="Opening Entry") { - return frappe.call({ - type:"GET", - method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_opening_accounts", - args: { - "company": frm.doc.company - }, - callback: function(r) { - frappe.model.clear_table(frm.doc, "accounts"); - if(r.message) { - update_jv_details(frm.doc, r.message); - } - cur_frm.set_value("is_opening", "Yes"); - } - }); - } } }, diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 4493c722544..8e5ba3718f7 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -137,7 +137,8 @@ "fieldname": "finance_book", "fieldtype": "Link", "label": "Finance Book", - "options": "Finance Book" + "options": "Finance Book", + "read_only": 1 }, { "fieldname": "2_add_edit_gl_entries", @@ -538,7 +539,7 @@ "idx": 176, "is_submittable": 1, "links": [], - "modified": "2022-04-06 17:18:46.865259", + "modified": "2022-06-23 22:01:32.348337", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 8660c18bf95..c46b185e60a 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -1192,24 +1192,6 @@ def get_payment_entry(ref_doc, args): return je if args.get("journal_entry") else je.as_dict() -@frappe.whitelist() -def get_opening_accounts(company): - """get all balance sheet accounts for opening entry""" - accounts = frappe.db.sql_list( - """select - name from tabAccount - where - is_group=0 and report_type='Balance Sheet' and company={0} and - name not in (select distinct account from tabWarehouse where - account is not null and account != '') - order by name asc""".format( - frappe.db.escape(company) - ) - ) - - return [{"account": a, "balance": get_balance_on(a)} for a in accounts] - - @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_against_jv(doctype, txt, searchfield, start, page_len, filters): diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index ccee7d9c548..6a3b38ae7b5 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -468,7 +468,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): update_value_in_dict(totals, "opening", gle) update_value_in_dict(totals, "closing", gle) - elif gle.posting_date <= to_date: + elif gle.posting_date <= to_date or (cstr(gle.is_opening) == "Yes" and show_opening_entries): if not group_by_voucher_consolidated: update_value_in_dict(gle_map[group_by_value].totals, "total", gle) update_value_in_dict(gle_map[group_by_value].totals, "closing", gle) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 651ab9b00e8..4d63a285534 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -257,6 +257,7 @@ class Asset(AccountsController): number_of_pending_depreciations += 1 skip_row = False + should_get_last_day = is_last_day_of_the_month(finance_book.depreciation_start_date) for n in range(start[finance_book.idx - 1], number_of_pending_depreciations): # If depreciation is already completed (for double declining balance) @@ -270,6 +271,9 @@ class Asset(AccountsController): finance_book.depreciation_start_date, n * cint(finance_book.frequency_of_depreciation) ) + if should_get_last_day: + schedule_date = get_last_day(schedule_date) + # schedule date will be a year later from start date # so monthly schedule date is calculated by removing 11 months from it monthly_schedule_date = add_months(schedule_date, -finance_book.frequency_of_depreciation + 1) @@ -845,14 +849,9 @@ class Asset(AccountsController): if args.get("rate_of_depreciation") and on_validate: return args.get("rate_of_depreciation") - no_of_years = ( - flt(args.get("total_number_of_depreciations") * flt(args.get("frequency_of_depreciation"))) - / 12 - ) value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount) - # square root of flt(salvage_value) / flt(asset_cost) - depreciation_rate = math.pow(value, 1.0 / flt(no_of_years, 2)) + depreciation_rate = math.pow(value, 1.0 / flt(args.get("total_number_of_depreciations"), 2)) return 100 * (1 - flt(depreciation_rate, float_precision)) @@ -1103,9 +1102,18 @@ def is_cwip_accounting_enabled(asset_category): def get_total_days(date, frequency): period_start_date = add_months(date, cint(frequency) * -1) + if is_last_day_of_the_month(date): + period_start_date = get_last_day(period_start_date) + return date_diff(date, period_start_date) +def is_last_day_of_the_month(date): + last_day_of_the_month = get_last_day(date) + + return getdate(last_day_of_the_month) == getdate(date) + + @erpnext.allow_regional def get_depreciation_amount(asset, depreciable_value, row): if row.depreciation_method in ("Straight Line", "Manual"): diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 226a38a5849..9bfafaf8657 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -701,6 +701,39 @@ class TestDepreciationMethods(AssetSetup): asset.reload() self.assertEquals(asset.finance_books[0].value_after_depreciation, 98000.0) + def test_monthly_depreciation_by_wdv_method(self): + asset = create_asset( + calculate_depreciation=1, + available_for_use_date="2022-02-15", + purchase_date="2022-02-15", + depreciation_method="Written Down Value", + gross_purchase_amount=10000, + expected_value_after_useful_life=5000, + depreciation_start_date="2022-02-28", + total_number_of_depreciations=5, + frequency_of_depreciation=1, + ) + + expected_schedules = [ + ["2022-02-28", 645.0, 645.0], + ["2022-03-31", 1206.8, 1851.8], + ["2022-04-30", 1051.12, 2902.92], + ["2022-05-31", 915.52, 3818.44], + ["2022-06-30", 797.42, 4615.86], + ["2022-07-15", 384.14, 5000.0], + ] + + schedules = [ + [ + cstr(d.schedule_date), + flt(d.depreciation_amount, 2), + flt(d.accumulated_depreciation_amount, 2), + ] + for d in asset.get("schedules") + ] + + self.assertEqual(schedules, expected_schedules) + class TestDepreciationBasics(AssetSetup): def test_depreciation_without_pro_rata(self): @@ -787,7 +820,7 @@ class TestDepreciationBasics(AssetSetup): expected_values = [["2020-12-31", 30000.0], ["2021-12-31", 30000.0], ["2022-12-31", 30000.0]] for i, schedule in enumerate(asset.schedules): - self.assertEqual(expected_values[i][0], schedule.schedule_date) + self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) self.assertEqual(expected_values[i][1], schedule.depreciation_amount) def test_set_accumulated_depreciation(self): @@ -1261,6 +1294,32 @@ class TestDepreciationBasics(AssetSetup): asset.cost_center = "Main - _TC" asset.submit() + def test_depreciation_on_final_day_of_the_month(self): + """Tests if final day of the month is picked each time, if the depreciation start date is the last day of the month.""" + + asset = create_asset( + item_code="Macbook Pro", + calculate_depreciation=1, + purchase_date="2020-01-30", + available_for_use_date="2020-02-15", + depreciation_start_date="2020-02-29", + frequency_of_depreciation=1, + total_number_of_depreciations=5, + submit=1, + ) + + expected_dates = [ + "2020-02-29", + "2020-03-31", + "2020-04-30", + "2020-05-31", + "2020-06-30", + "2020-07-15", + ] + + for i, schedule in enumerate(asset.schedules): + self.assertEqual(getdate(expected_dates[i]), getdate(schedule.schedule_date)) + def create_asset_data(): if not frappe.db.exists("Asset Category", "Computers"): diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index ed0f9002a21..b6f5ff92191 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -425,7 +425,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( company: me.frm.doc.company }, allow_child_item_selection: true, - child_fielname: "items", + child_fieldname: "items", child_columns: ["item_code", "qty"] }) }, __("Get Items From")); diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index e4fb970c3f7..590370e808c 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -140,6 +140,43 @@ class TestPurchaseOrder(FrappeTestCase): # ordered qty decreases as ordered qty is 0 (deleted row) self.assertEqual(get_ordered_qty(), existing_ordered_qty - 10) # 0 + def test_supplied_items_validations_on_po_update_after_submit(self): + po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes", qty=5, rate=100) + item = po.items[0] + + original_supplied_items = {po.name: po.required_qty for po in po.supplied_items} + + # Just update rate + trans_item = [ + { + "item_code": "_Test FG Item", + "rate": 20, + "qty": 5, + "conversion_factor": 1.0, + "docname": item.name, + } + ] + update_child_qty_rate("Purchase Order", json.dumps(trans_item), po.name) + po.reload() + + new_supplied_items = {po.name: po.required_qty for po in po.supplied_items} + self.assertEqual(set(original_supplied_items.keys()), set(new_supplied_items.keys())) + + # Update qty to 2x + trans_item[0]["qty"] *= 2 + update_child_qty_rate("Purchase Order", json.dumps(trans_item), po.name) + po.reload() + + new_supplied_items = {po.name: po.required_qty for po in po.supplied_items} + self.assertEqual(2 * sum(original_supplied_items.values()), sum(new_supplied_items.values())) + + # Set transfer qty and attempt to update qty, shouldn't be allowed + po.supplied_items[0].supplied_qty = 2 + po.supplied_items[0].db_update() + trans_item[0]["qty"] *= 2 + with self.assertRaises(frappe.ValidationError): + update_child_qty_rate("Purchase Order", json.dumps(trans_item), po.name) + def test_update_child(self): mr = make_material_request(qty=10) po = make_purchase_order(mr.name) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 71901c52bf6..dce6d7525d0 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2438,7 +2438,7 @@ def update_bin_on_delete(row, doctype): update_bin_qty(row.item_code, row.warehouse, qty_dict) -def validate_and_delete_children(parent, data): +def validate_and_delete_children(parent, data) -> bool: deleted_children = [] updated_item_names = [d.get("docname") for d in data] for item in parent.items: @@ -2457,6 +2457,8 @@ def validate_and_delete_children(parent, data): for d in deleted_children: update_bin_on_delete(d, parent.doctype) + return bool(deleted_children) + @frappe.whitelist() def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"): @@ -2520,13 +2522,38 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil ): frappe.throw(_("Cannot set quantity less than received quantity")) + def should_update_supplied_items(doc) -> bool: + """Subcontracted PO can allow following changes *after submit*: + + 1. Change rate of subcontracting - regardless of other changes. + 2. Change qty and/or add new items and/or remove items + Exception: Transfer/Consumption is already made, qty change not allowed. + """ + + supplied_items_processed = any( + item.supplied_qty or item.consumed_qty or item.returned_qty for item in doc.supplied_items + ) + + update_supplied_items = ( + any_qty_changed or items_added_or_removed or any_conversion_factor_changed + ) + if update_supplied_items and supplied_items_processed: + frappe.throw(_("Item qty can not be updated as raw materials are already processed.")) + + return update_supplied_items + data = json.loads(trans_items) + any_qty_changed = False # updated to true if any item's qty changes + 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") - validate_and_delete_children(parent, data) + _removed_items = validate_and_delete_children(parent, data) + items_added_or_removed |= _removed_items for d in data: new_child_flag = False @@ -2537,6 +2564,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil if not d.get("docname"): new_child_flag = True + items_added_or_removed = True check_doc_permissions(parent, "create") child_item = get_new_child_item(d) else: @@ -2559,6 +2587,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil qty_unchanged = prev_qty == new_qty uom_unchanged = prev_uom == new_uom conversion_factor_unchanged = prev_con_fac == new_con_fac + any_conversion_factor_changed |= not conversion_factor_unchanged date_unchanged = ( prev_date == getdate(new_date) if prev_date and new_date else False ) # in case of delivery note etc @@ -2572,6 +2601,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil continue validate_quantity(child_item, d) + if flt(child_item.get("qty")) != flt(d.get("qty")): + any_qty_changed = True child_item.qty = flt(d.get("qty")) rate_precision = child_item.precision("rate") or 2 @@ -2677,8 +2708,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil parent.update_ordered_and_reserved_qty() parent.update_receiving_percentage() if parent.is_subcontracted == "Yes": - parent.update_reserved_qty_for_subcontract() - parent.create_raw_materials_supplied("supplied_items") + if should_update_supplied_items(parent): + parent.update_reserved_qty_for_subcontract() + parent.create_raw_materials_supplied("supplied_items") parent.save() else: # Sales Order parent.validate_warehouse() diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 20b84b5cb90..386ac0a2888 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -461,6 +461,14 @@ scheduler_events = { "0/30 * * * *": [ "erpnext.utilities.doctype.video.video.update_youtube_data", ], + # Hourly but offset by 30 minutes + "30 * * * *": [ + "erpnext.accounts.doctype.gl_entry.gl_entry.rename_gle_sle_docs", + ], + # Daily but offset by 45 minutes + "45 0 * * *": [ + "erpnext.stock.reorder_item.reorder_item", + ], }, "all": [ "erpnext.projects.doctype.project.project.project_status_update_reminder", @@ -472,7 +480,6 @@ scheduler_events = { "erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails", "erpnext.accounts.doctype.subscription.subscription.process_all", "erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_settings.schedule_get_order_details", - "erpnext.accounts.doctype.gl_entry.gl_entry.rename_gle_sle_docs", "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization", "erpnext.projects.doctype.project.project.hourly_reminder", "erpnext.projects.doctype.project.project.collect_project_status", @@ -484,7 +491,6 @@ scheduler_events = { "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries" ], "daily": [ - "erpnext.stock.reorder_item.reorder_item", "erpnext.support.doctype.issue.issue.auto_close_tickets", "erpnext.crm.doctype.opportunity.opportunity.auto_close_opportunity", "erpnext.controllers.accounts_controller.update_invoice_status", diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index ab218a76d55..396ade0ef13 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -713,7 +713,7 @@ erpnext.utils.map_current_doc = function(opts) { get_query: opts.get_query, add_filters_group: 1, allow_child_item_selection: opts.allow_child_item_selection, - child_fieldname: opts.child_fielname, + child_fieldname: opts.child_fieldname, child_columns: opts.child_columns, size: opts.size, action: function(selections, args) { diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.js b/erpnext/selling/doctype/product_bundle/product_bundle.js index 7a04c6ab06d..3096b692a7e 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.js +++ b/erpnext/selling/doctype/product_bundle/product_bundle.js @@ -1,19 +1,13 @@ -// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -cur_frm.cscript.refresh = function(doc, cdt, cdn) { - cur_frm.toggle_enable('new_item_code', doc.__islocal); -} - -cur_frm.fields_dict.new_item_code.get_query = function() { - return{ - query: "erpnext.selling.doctype.product_bundle.product_bundle.get_new_item_code" - } -} -cur_frm.fields_dict.new_item_code.query_description = __('Please select Item where "Is Stock Item" is "No" and "Is Sales Item" is "Yes" and there is no other Product Bundle'); - -cur_frm.cscript.onload = function() { - // set add fetch for item_code's item_name and description - cur_frm.add_fetch('item_code', 'stock_uom', 'uom'); - cur_frm.add_fetch('item_code', 'description', 'description'); -} +frappe.ui.form.on("Product Bundle", { + refresh: function (frm) { + frm.toggle_enable("new_item_code", frm.is_new()); + frm.set_query("new_item_code", () => { + return { + query: "erpnext.selling.doctype.product_bundle.product_bundle.get_new_item_code", + }; + }); + }, +}); diff --git a/erpnext/selling/doctype/product_bundle_item/product_bundle_item.json b/erpnext/selling/doctype/product_bundle_item/product_bundle_item.json index dc071e4d65e..fc8caeb31d9 100644 --- a/erpnext/selling/doctype/product_bundle_item/product_bundle_item.json +++ b/erpnext/selling/doctype/product_bundle_item/product_bundle_item.json @@ -33,6 +33,8 @@ "reqd": 1 }, { + "fetch_from": "item_code.description", + "fetch_if_empty": 1, "fieldname": "description", "fieldtype": "Text Editor", "in_list_view": 1, @@ -51,6 +53,8 @@ "print_hide": 1 }, { + "fetch_from": "item_code.stock_uom", + "fetch_if_empty": 1, "fieldname": "uom", "fieldtype": "Link", "in_list_view": 1, @@ -64,7 +68,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-02-28 14:06:05.725655", + "modified": "2022-06-27 05:30:18.475150", "modified_by": "Administrator", "module": "Selling", "name": "Product Bundle Item", @@ -72,5 +76,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index d1530395d8a..ce40d1122ed 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -204,6 +204,15 @@ def make_sales_order(source_name, target_doc=None): 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", + {"prevdoc_docname": source_name, "docstatus": 1}, + ["item_code", "sum(qty)"], + group_by="item_code", + as_list=1, + ) + ) def set_missing_values(source, target): if customer: @@ -219,7 +228,9 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): target.run_method("calculate_taxes_and_totals") def update_item(obj, target, source_parent): - target.stock_qty = flt(obj.qty) * flt(obj.conversion_factor) + balance_qty = obj.qty - ordered_items.get(obj.item_code, 0.0) + target.qty = balance_qty if balance_qty > 0 else 0 + target.stock_qty = flt(target.qty) * flt(obj.conversion_factor) if obj.against_blanket_order: target.against_blanket_order = obj.against_blanket_order @@ -235,6 +246,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): "doctype": "Sales Order Item", "field_map": {"parent": "prevdoc_docname"}, "postprocess": update_item, + "condition": lambda doc: doc.qty > 0, }, "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, "Sales Team": {"doctype": "Sales Team", "add_if_empty": True},