diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index afabe43fec..2cf44444cc 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -115,4 +115,5 @@ jobs: echo "Updating to latest version" git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}" git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA" + bench setup requirements --python bench --site test_site migrate diff --git a/.mergify.yml b/.mergify.yml index cc8c0802f1..d7f82e696b 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -9,6 +9,8 @@ pull_request_rules: - author!=nabinhait - author!=ankush - author!=deepeshgarg007 + - author!=mergify[bot] + - or: - base=version-13 - base=version-12 @@ -19,6 +21,16 @@ pull_request_rules: @{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch. https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch + - name: Auto-close PRs on pre-release branch + conditions: + - base=version-13-pre-release + actions: + close: + comment: + message: | + @{{author}}, pre-release branch is not maintained anymore. Releases are directly done by merging hotfix branch to stable branches. + + - name: backport to develop conditions: - label="backport develop" diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index c71ea3648b..2610c8655e 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -322,9 +322,9 @@ def get_parent_account(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql( """select name from tabAccount where is_group = 1 and docstatus != 2 and company = %s - and %s like %s order by name limit %s, %s""" + and %s like %s order by name limit %s offset %s""" % ("%s", searchfield, "%s", "%s", "%s"), - (filters["company"], "%%%s%%" % txt, start, page_len), + (filters["company"], "%%%s%%" % txt, page_len, start), as_list=1, ) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 145118957b..8f0fe51e3d 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -1239,7 +1239,7 @@ def get_against_jv(doctype, txt, searchfield, start, page_len, filters): AND jv.docstatus = 1 AND jv.`{0}` LIKE %(txt)s ORDER BY jv.name DESC - LIMIT %(offset)s, %(limit)s + LIMIT %(limit)s offset %(offset)s """.format( searchfield ), diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index a10a810d1d..f7a57bb96e 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1444,7 +1444,7 @@ def get_negative_outstanding_invoices( voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice" supplier_condition = "" if voucher_type == "Purchase Invoice": - supplier_condition = "and (release_date is null or release_date <= CURDATE())" + supplier_condition = "and (release_date is null or release_date <= CURRENT_DATE)" if party_account_currency == company_currency: grand_total_field = "base_grand_total" rounded_total_field = "base_rounded_total" diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json index d96107678f..39e90420c7 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json +++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json @@ -1,7 +1,6 @@ { "actions": [], "allow_rename": 1, - "autoname": "format:PLE-{YY}-{MM}-{######}", "creation": "2022-05-09 19:35:03.334361", "doctype": "DocType", "editable_grid": 1, @@ -138,11 +137,10 @@ "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-05-19 18:04:44.609115", + "modified": "2022-05-30 19:04:55.532171", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Ledger Entry", - "naming_rule": "Expression", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/accounts/doctype/payment_order/payment_order.py b/erpnext/accounts/doctype/payment_order/payment_order.py index 3c45d20770..ff9615d14f 100644 --- a/erpnext/accounts/doctype/payment_order/payment_order.py +++ b/erpnext/accounts/doctype/payment_order/payment_order.py @@ -39,7 +39,7 @@ def get_mop_query(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql( """ select mode_of_payment from `tabPayment Order Reference` where parent = %(parent)s and mode_of_payment like %(txt)s - limit %(start)s, %(page_len)s""", + limit %(page_len)s offset %(start)s""", {"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt}, ) @@ -51,7 +51,7 @@ def get_supplier_query(doctype, txt, searchfield, start, page_len, filters): """ select supplier from `tabPayment Order Reference` where parent = %(parent)s and supplier like %(txt)s and (payment_reference is null or payment_reference='') - limit %(start)s, %(page_len)s""", + limit %(page_len)s offset %(start)s""", {"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt}, ) diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py index e83dc0f11e..e8aee737f2 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py @@ -173,7 +173,7 @@ def pos_profile_query(doctype, txt, searchfield, start, page_len, filters): where pfu.parent = pf.name and pfu.user = %(user)s and pf.company = %(company)s and (pf.name like %(txt)s) - and pf.disabled = 0 limit %(start)s, %(page_len)s""", + and pf.disabled = 0 limit %(page_len)s offset %(start)s""", args, ) diff --git a/erpnext/accounts/module_onboarding/accounts/accounts.json b/erpnext/accounts/module_onboarding/accounts/accounts.json index b9040e3309..9916d1622d 100644 --- a/erpnext/accounts/module_onboarding/accounts/accounts.json +++ b/erpnext/accounts/module_onboarding/accounts/accounts.json @@ -13,7 +13,7 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/accounts", "idx": 0, "is_complete": 0, - "modified": "2022-06-07 14:29:21.352132", + "modified": "2022-06-14 17:38:24.967834", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts", diff --git a/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json b/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json index b6e9f5cd87..e323f6cb1a 100644 --- a/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json +++ b/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json @@ -2,14 +2,14 @@ "action": "Create Entry", "action_label": "Manage Sales Tax Templates", "creation": "2020-05-13 19:29:43.844463", - "description": "# Setting up Taxes\n\nERPNext lets you configure your taxes so that they are automatically applied in your buying and selling transactions. You can configure them globally or even on Items. ERPNext taxes are pre-configured for most regions.\n\n[Checkout pre-configured taxes](/app/sales-taxes-and-charges-template)\n", + "description": "# Setting up Taxes\n\nERPNext lets you configure your taxes so that they are automatically applied in your buying and selling transactions. You can configure them globally or even on Items. ERPNext taxes are pre-configured for most regions.", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2022-06-07 14:27:15.906286", + "modified": "2022-06-14 17:37:56.694261", "modified_by": "Administrator", "name": "Setup Taxes", "owner": "Administrator", diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index f4a44bd362..e39f22b4cf 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -211,7 +211,7 @@ def set_address_details( else: party_details.update(get_company_address(company)) - if doctype and doctype in ["Delivery Note", "Sales Invoice", "Sales Order"]: + if doctype and doctype in ["Delivery Note", "Sales Invoice", "Sales Order", "Quotation"]: if party_details.company_address: party_details.update( get_fetch_values(doctype, "company_address", party_details.company_address) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index 748bcde435..0238711a70 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -172,11 +172,6 @@ frappe.query_reports["Accounts Receivable"] = { "label": __("Show Sales Person"), "fieldtype": "Check", }, - { - "fieldname": "show_remarks", - "label": __("Show Remarks"), - "fieldtype": "Check", - }, { "fieldname": "tax_id", "label": __("Tax Id"), diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index de9d63d849..1911152dec 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -5,7 +5,9 @@ from collections import OrderedDict import frappe -from frappe import _, scrub +from frappe import _, qb, scrub +from frappe.query_builder import Criterion +from frappe.query_builder.functions import Date from frappe.utils import cint, cstr, flt, getdate, nowdate from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( @@ -41,6 +43,8 @@ def execute(filters=None): class ReceivablePayableReport(object): def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) + self.qb_selection_filter = [] + self.ple = qb.DocType("Payment Ledger Entry") self.filters.report_date = getdate(self.filters.report_date or nowdate()) self.age_as_on = ( getdate(nowdate()) @@ -78,7 +82,7 @@ class ReceivablePayableReport(object): self.skip_total_row = 1 def get_data(self): - self.get_gl_entries() + self.get_ple_entries() self.get_sales_invoices_or_customers_based_on_sales_person() self.voucher_balance = OrderedDict() self.init_voucher_balance() # invoiced, paid, credit_note, outstanding @@ -96,25 +100,25 @@ class ReceivablePayableReport(object): self.get_return_entries() self.data = [] - for gle in self.gl_entries: - self.update_voucher_balance(gle) + + for ple in self.ple_entries: + self.update_voucher_balance(ple) self.build_data() def init_voucher_balance(self): # build all keys, since we want to exclude vouchers beyond the report date - for gle in self.gl_entries: + for ple in self.ple_entries: # get the balance object for voucher_type - key = (gle.voucher_type, gle.voucher_no, gle.party) + key = (ple.voucher_type, ple.voucher_no, ple.party) if not key in self.voucher_balance: self.voucher_balance[key] = frappe._dict( - voucher_type=gle.voucher_type, - voucher_no=gle.voucher_no, - party=gle.party, - party_account=gle.account, - posting_date=gle.posting_date, - account_currency=gle.account_currency, - remarks=gle.remarks if self.filters.get("show_remarks") else None, + 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, invoiced=0.0, paid=0.0, credit_note=0.0, @@ -124,23 +128,22 @@ class ReceivablePayableReport(object): credit_note_in_account_currency=0.0, outstanding_in_account_currency=0.0, ) - self.get_invoices(gle) if self.filters.get("group_by_party"): - self.init_subtotal_row(gle.party) + self.init_subtotal_row(ple.party) if self.filters.get("group_by_party"): self.init_subtotal_row("Total") - def get_invoices(self, gle): - if gle.voucher_type in ("Sales Invoice", "Purchase Invoice"): + def get_invoices(self, ple): + if ple.voucher_type in ("Sales Invoice", "Purchase Invoice"): if self.filters.get("sales_person"): - if gle.voucher_no in self.sales_person_records.get( + if ple.voucher_no in self.sales_person_records.get( "Sales Invoice", [] - ) or gle.party in self.sales_person_records.get("Customer", []): - self.invoices.add(gle.voucher_no) + ) or ple.party in self.sales_person_records.get("Customer", []): + self.invoices.add(ple.voucher_no) else: - self.invoices.add(gle.voucher_no) + self.invoices.add(ple.voucher_no) def init_subtotal_row(self, party): if not self.total_row_map.get(party): @@ -162,39 +165,49 @@ class ReceivablePayableReport(object): "range5", ] - def update_voucher_balance(self, gle): + def get_voucher_balance(self, ple): + if self.filters.get("sales_person"): + if not ( + ple.party in self.sales_person_records.get("Customer", []) + or ple.against_voucher_no in self.sales_person_records.get("Sales Invoice", []) + ): + return + + key = (ple.against_voucher_type, ple.against_voucher_no, ple.party) + row = self.voucher_balance.get(key) + return row + + def update_voucher_balance(self, ple): # get the row where this balance needs to be updated # if its a payment, it will return the linked invoice or will be considered as advance - row = self.get_voucher_balance(gle) + row = self.get_voucher_balance(ple) if not row: return - # gle_balance will be the total "debit - credit" for receivable type reports and - # and vice-versa for payable type reports - gle_balance = self.get_gle_balance(gle) - gle_balance_in_account_currency = self.get_gle_balance_in_account_currency(gle) - if gle_balance > 0: - if gle.voucher_type in ("Journal Entry", "Payment Entry") and gle.against_voucher: - # debit against sales / purchase invoice - row.paid -= gle_balance - row.paid_in_account_currency -= gle_balance_in_account_currency + amount = ple.amount + amount_in_account_currency = ple.amount_in_account_currency + + # update voucher + if ple.amount > 0: + if ( + ple.voucher_type in ["Journal Entry", "Payment Entry"] + and ple.voucher_no != ple.against_voucher_no + ): + row.paid -= amount + row.paid_in_account_currency -= amount_in_account_currency else: - # invoice - row.invoiced += gle_balance - row.invoiced_in_account_currency += gle_balance_in_account_currency + row.invoiced += amount + row.invoiced_in_account_currency += amount_in_account_currency else: - # payment or credit note for receivables - if self.is_invoice(gle): - # stand alone debit / credit note - row.credit_note -= gle_balance - row.credit_note_in_account_currency -= gle_balance_in_account_currency + if self.is_invoice(ple): + row.credit_note -= amount + row.credit_note_in_account_currency -= amount_in_account_currency else: - # advance / unlinked payment or other adjustment - row.paid -= gle_balance - row.paid_in_account_currency -= gle_balance_in_account_currency + row.paid -= amount + row.paid_in_account_currency -= amount_in_account_currency - if gle.cost_center: - row.cost_center = str(gle.cost_center) + if ple.cost_center: + row.cost_center = str(ple.cost_center) def update_sub_total_row(self, row, party): total_row = self.total_row_map.get(party) @@ -210,39 +223,6 @@ class ReceivablePayableReport(object): self.data.append({}) self.update_sub_total_row(sub_total_row, "Total") - def get_voucher_balance(self, gle): - if self.filters.get("sales_person"): - against_voucher = gle.against_voucher or gle.voucher_no - if not ( - gle.party in self.sales_person_records.get("Customer", []) - or against_voucher in self.sales_person_records.get("Sales Invoice", []) - ): - return - - voucher_balance = None - if gle.against_voucher: - # find invoice - against_voucher = gle.against_voucher - - # If payment is made against credit note - # and credit note is made against a Sales Invoice - # then consider the payment against original sales invoice. - if gle.against_voucher_type in ("Sales Invoice", "Purchase Invoice"): - if gle.against_voucher in self.return_entries: - return_against = self.return_entries.get(gle.against_voucher) - if return_against: - against_voucher = return_against - - voucher_balance = self.voucher_balance.get( - (gle.against_voucher_type, against_voucher, gle.party) - ) - - if not voucher_balance: - # no invoice, this is an invoice / stand-alone payment / credit note - voucher_balance = self.voucher_balance.get((gle.voucher_type, gle.voucher_no, gle.party)) - - return voucher_balance - def build_data(self): # set outstanding for all the accumulated balances # as we can use this to filter out invoices without outstanding @@ -260,6 +240,7 @@ class ReceivablePayableReport(object): if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and ( abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision ): + # non-zero oustanding, we must consider this row if self.is_invoice(row) and self.filters.based_on_payment_terms: @@ -669,48 +650,53 @@ class ReceivablePayableReport(object): index = 4 row["range" + str(index + 1)] = row.outstanding - def get_gl_entries(self): + def get_ple_entries(self): # get all the GL entries filtered by the given filters - conditions, values = self.prepare_conditions() - order_by = self.get_order_by_condition() + self.prepare_conditions() if self.filters.show_future_payments: - values.insert(2, self.filters.report_date) - - date_condition = """AND (posting_date <= %s - OR (against_voucher IS NULL AND DATE(creation) <= %s))""" + self.qb_selection_filter.append( + ( + self.ple.posting_date.lte(self.filters.report_date) + | ( + (self.ple.voucher_no == self.ple.against_voucher_no) + & (Date(self.ple.creation).lte(self.filters.report_date)) + ) + ) + ) else: - date_condition = "AND posting_date <=%s" + self.qb_selection_filter.append(self.ple.posting_date.lte(self.filters.report_date)) - if self.filters.get(scrub(self.party_type)): - select_fields = "debit_in_account_currency as debit, credit_in_account_currency as credit" - else: - select_fields = "debit, credit" - - doc_currency_fields = "debit_in_account_currency, credit_in_account_currency" - - remarks = ", remarks" if self.filters.get("show_remarks") else "" - - self.gl_entries = frappe.db.sql( - """ - select - name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center, - against_voucher_type, against_voucher, account_currency, {0}, {1} {remarks} - from - `tabGL Entry` - where - docstatus < 2 - and is_cancelled = 0 - and party_type=%s - and (party is not null and party != '') - {2} {3} {4}""".format( - select_fields, doc_currency_fields, date_condition, conditions, order_by, remarks=remarks - ), - values, - as_dict=True, + ple = qb.DocType("Payment Ledger Entry") + query = ( + qb.from_(ple) + .select( + ple.account, + ple.voucher_type, + ple.voucher_no, + ple.against_voucher_type, + ple.against_voucher_no, + ple.party_type, + ple.cost_center, + ple.party, + ple.posting_date, + ple.due_date, + ple.account_currency.as_("currency"), + ple.amount, + ple.amount_in_account_currency, + ) + .where(ple.delinked == 0) + .where(Criterion.all(self.qb_selection_filter)) ) + if self.filters.get("group_by_party"): + query = query.orderby(self.ple.party, self.ple.posting_date) + else: + query = query.orderby(self.ple.posting_date, self.ple.party) + + self.ple_entries = query.run(as_dict=True) + 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"]) @@ -731,23 +717,21 @@ class ReceivablePayableReport(object): self.sales_person_records.setdefault(d.parenttype, set()).add(d.parent) def prepare_conditions(self): - conditions = [""] - values = [self.party_type, self.filters.report_date] + self.qb_selection_filter = [] party_type_field = scrub(self.party_type) - self.add_common_filters(conditions, values, party_type_field) + self.add_common_filters(party_type_field=party_type_field) if party_type_field == "customer": - self.add_customer_filters(conditions, values) + self.add_customer_filters() elif party_type_field == "supplier": - self.add_supplier_filters(conditions, values) + self.add_supplier_filters() if self.filters.cost_center: - self.get_cost_center_conditions(conditions) + self.get_cost_center_conditions() - self.add_accounting_dimensions_filters(conditions, values) - return " and ".join(conditions), values + self.add_accounting_dimensions_filters() def get_cost_center_conditions(self, conditions): lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"]) @@ -755,32 +739,20 @@ class ReceivablePayableReport(object): center.name for center in frappe.get_list("Cost Center", filters={"lft": (">=", lft), "rgt": ("<=", rgt)}) ] + self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list)) - cost_center_string = '", "'.join(cost_center_list) - conditions.append('cost_center in ("{0}")'.format(cost_center_string)) - - def get_order_by_condition(self): - if self.filters.get("group_by_party"): - return "order by party, posting_date" - else: - return "order by posting_date, party" - - def add_common_filters(self, conditions, values, party_type_field): + def add_common_filters(self, party_type_field): if self.filters.company: - conditions.append("company=%s") - values.append(self.filters.company) + self.qb_selection_filter.append(self.ple.company == self.filters.company) if self.filters.finance_book: - conditions.append("ifnull(finance_book, '') in (%s, '')") - values.append(self.filters.finance_book) + self.qb_selection_filter.append(self.ple.finance_book == self.filters.finance_book) if self.filters.get(party_type_field): - conditions.append("party=%s") - values.append(self.filters.get(party_type_field)) + self.qb_selection_filter.append(self.ple.party == self.filters.get(party_type_field)) if self.filters.party_account: - conditions.append("account =%s") - values.append(self.filters.party_account) + self.qb_selection_filter.append(self.ple.account == self.filters.party_account) else: # get GL with "receivable" or "payable" account_type account_type = "Receivable" if self.party_type == "Customer" else "Payable" @@ -792,46 +764,68 @@ class ReceivablePayableReport(object): ] if accounts: - conditions.append("account in (%s)" % ",".join(["%s"] * len(accounts))) - values += accounts + self.qb_selection_filter.append(self.ple.account.isin(accounts)) + + def add_customer_filters( + self, + ): + self.customter = qb.DocType("Customer") - def add_customer_filters(self, conditions, values): if self.filters.get("customer_group"): - conditions.append(self.get_hierarchical_filters("Customer Group", "customer_group")) + self.get_hierarchical_filters("Customer Group", "customer_group") if self.filters.get("territory"): - conditions.append(self.get_hierarchical_filters("Territory", "territory")) + self.get_hierarchical_filters("Territory", "territory") if self.filters.get("payment_terms_template"): - conditions.append("party in (select name from tabCustomer where payment_terms=%s)") - values.append(self.filters.get("payment_terms_template")) + self.qb_selection_filter.append( + self.ple.party_isin( + qb.from_(self.customer).where( + self.customer.payment_terms == self.filters.get("payment_terms_template") + ) + ) + ) if self.filters.get("sales_partner"): - conditions.append("party in (select name from tabCustomer where default_sales_partner=%s)") - values.append(self.filters.get("sales_partner")) - - def add_supplier_filters(self, conditions, values): - if self.filters.get("supplier_group"): - conditions.append( - """party in (select name from tabSupplier - where supplier_group=%s)""" + self.qb_selection_filter.append( + self.ple.party_isin( + qb.from_(self.customer).where( + self.customer.default_sales_partner == self.filters.get("payment_terms_template") + ) + ) + ) + + def add_supplier_filters(self): + supplier = qb.DocType("Supplier") + if self.filters.get("supplier_group"): + self.qb_selection_filter.append( + self.ple.party.isin( + qb.from_(supplier) + .select(supplier.name) + .where(supplier.supplier_group == self.filters.get("supplier_group")) + ) ) - values.append(self.filters.get("supplier_group")) if self.filters.get("payment_terms_template"): - conditions.append("party in (select name from tabSupplier where payment_terms=%s)") - values.append(self.filters.get("payment_terms_template")) + self.qb_selection_filter.append( + self.ple.party.isin( + qb.from_(supplier) + .select(supplier.name) + .where(supplier.payment_terms == self.filters.get("supplier_group")) + ) + ) def get_hierarchical_filters(self, doctype, key): lft, rgt = frappe.db.get_value(doctype, self.filters.get(key), ["lft", "rgt"]) - return """party in (select name from tabCustomer - where exists(select name from `tab{doctype}` where lft >= {lft} and rgt <= {rgt} - and name=tabCustomer.{key}))""".format( - doctype=doctype, lft=lft, rgt=rgt, key=key - ) + doc = qb.DocType(doctype) + ple = self.ple + customer = self.customer + groups = qb.from_(doc).select(doc.name).where((doc.lft >= lft) & (doc.rgt <= rgt)) + customers = qb.from_(customer).select(customer.name).where(customer[key].isin(groups)) + self.qb_selection_filter.append(ple.isin(ple.party.isin(customers))) - def add_accounting_dimensions_filters(self, conditions, values): + def add_accounting_dimensions_filters(self): accounting_dimensions = get_accounting_dimensions(as_list=False) if accounting_dimensions: @@ -841,30 +835,16 @@ class ReceivablePayableReport(object): self.filters[dimension.fieldname] = get_dimension_with_children( dimension.document_type, self.filters.get(dimension.fieldname) ) - conditions.append("{0} in %s".format(dimension.fieldname)) - values.append(tuple(self.filters.get(dimension.fieldname))) + self.qb_selection_filter.append( + self.ple[dimension.fieldname].isin(self.filters[dimension.fieldname]) + ) + else: + self.qb_selection_filter.append( + self.ple[dimension.fieldname] == self.filters[dimension.fieldname] + ) - def get_gle_balance(self, gle): - # get the balance of the GL (debit - credit) or reverse balance based on report type - return gle.get(self.dr_or_cr) - self.get_reverse_balance(gle) - - def get_gle_balance_in_account_currency(self, gle): - # get the balance of the GL (debit - credit) or reverse balance based on report type - return gle.get( - self.dr_or_cr + "_in_account_currency" - ) - self.get_reverse_balance_in_account_currency(gle) - - def get_reverse_balance_in_account_currency(self, gle): - return gle.get( - "debit_in_account_currency" if self.dr_or_cr == "credit" else "credit_in_account_currency" - ) - - def get_reverse_balance(self, gle): - # get "credit" balance if report type is "debit" and vice versa - return gle.get("debit" if self.dr_or_cr == "credit" else "credit") - - def is_invoice(self, gle): - if gle.voucher_type in ("Sales Invoice", "Purchase Invoice"): + def is_invoice(self, ple): + if ple.voucher_type in ("Sales Invoice", "Purchase Invoice"): return True def get_party_details(self, party): @@ -926,9 +906,6 @@ class ReceivablePayableReport(object): width=180, ) - if self.filters.show_remarks: - self.add_column(label=_("Remarks"), fieldname="remarks", fieldtype="Text", width=200), - self.add_column(label="Due Date", fieldtype="Date") if self.party_type == "Supplier": diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index f38890e980..edddbbce21 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -12,6 +12,7 @@ class TestAccountsReceivable(unittest.TestCase): def test_accounts_receivable(self): frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'") frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'") + frappe.db.sql("delete from `tabPayment Ledger Entry` where company='_Test Company 2'") filters = { "company": "_Test Company 2", diff --git a/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py b/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py index 1a003993aa..230b18c293 100644 --- a/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py +++ b/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py @@ -100,7 +100,7 @@ def get_sales_details(filters): sales_data = frappe.db.sql( """ select s.territory, s.customer, si.item_group, si.item_code, si.qty, {date_field} as last_order_date, - DATEDIFF(CURDATE(), {date_field}) as days_since_last_order + DATEDIFF(CURRENT_DATE, {date_field}) as days_since_last_order from `tab{doctype}` s, `tab{doctype} Item` si where s.name = si.parent and s.docstatus = 1 order by days_since_last_order """.format( # nosec diff --git a/erpnext/accounts/test/test_utils.py b/erpnext/accounts/test/test_utils.py index 77c40bae2d..882cd694a3 100644 --- a/erpnext/accounts/test/test_utils.py +++ b/erpnext/accounts/test/test_utils.py @@ -62,8 +62,8 @@ class TestUtils(unittest.TestCase): stock_entry = {"item": item, "to_warehouse": "_Test Warehouse - _TC", "qty": 1, "rate": 10} se1 = make_stock_entry(posting_date="2022-01-01", **stock_entry) - se2 = make_stock_entry(posting_date="2022-02-01", **stock_entry) se3 = make_stock_entry(posting_date="2022-03-01", **stock_entry) + se2 = make_stock_entry(posting_date="2022-02-01", **stock_entry) for doc in (se1, se2, se3): vouchers.append((doc.doctype, doc.name)) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 8711395d55..2d86dead2f 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -2,14 +2,18 @@ # License: GNU General Public License v3. See license.txt +import itertools from json import loads -from typing import List, Tuple +from typing import TYPE_CHECKING, List, Optional, Tuple import frappe import frappe.defaults from frappe import _, qb, throw from frappe.model.meta import get_field_precision +from frappe.query_builder.utils import DocType from frappe.utils import cint, cstr, flt, formatdate, get_number_format_info, getdate, now, nowdate +from pypika import Order +from pypika.terms import ExistsCriterion import erpnext @@ -19,6 +23,9 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g from erpnext.stock import get_warehouse_account_map from erpnext.stock.utils import get_stock_value_on +if TYPE_CHECKING: + from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import RepostItemValuation + class FiscalYearError(frappe.ValidationError): pass @@ -28,6 +35,9 @@ class PaymentEntryUnlinkError(frappe.ValidationError): pass +GL_REPOSTING_CHUNK = 100 + + @frappe.whitelist() def get_fiscal_year( date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False @@ -42,37 +52,32 @@ def get_fiscal_years( if not fiscal_years: # if year start date is 2012-04-01, year end date should be 2013-03-31 (hence subdate) - cond = "" - if fiscal_year: - cond += " and fy.name = {0}".format(frappe.db.escape(fiscal_year)) - if company: - cond += """ - and (not exists (select name - from `tabFiscal Year Company` fyc - where fyc.parent = fy.name) - or exists(select company - from `tabFiscal Year Company` fyc - where fyc.parent = fy.name - and fyc.company=%(company)s) - ) - """ + FY = DocType("Fiscal Year") - fiscal_years = frappe.db.sql( - """ - select - fy.name, fy.year_start_date, fy.year_end_date - from - `tabFiscal Year` fy - where - disabled = 0 {0} - order by - fy.year_start_date desc""".format( - cond - ), - {"company": company}, - as_dict=True, + query = ( + frappe.qb.from_(FY) + .select(FY.name, FY.year_start_date, FY.year_end_date) + .where(FY.disabled == 0) ) + if fiscal_year: + query = query.where(FY.name == fiscal_year) + + if company: + FYC = DocType("Fiscal Year Company") + query = query.where( + ExistsCriterion(frappe.qb.from_(FYC).select(FYC.name).where(FYC.parent == FY.name)).negate() + | ExistsCriterion( + frappe.qb.from_(FYC) + .select(FYC.company) + .where(FYC.parent == FY.name) + .where(FYC.company == company) + ) + ) + + query = query.orderby(FY.year_start_date, Order.desc) + fiscal_years = query.run(as_dict=True) + frappe.cache().hset("fiscal_years", company, fiscal_years) if not transaction_date and not fiscal_year: @@ -1122,7 +1127,11 @@ def update_gl_entries_after( def repost_gle_for_stock_vouchers( - stock_vouchers, posting_date, company=None, warehouse_account=None + stock_vouchers: List[Tuple[str, str]], + posting_date: str, + company: Optional[str] = None, + warehouse_account=None, + repost_doc: Optional["RepostItemValuation"] = None, ): from erpnext.accounts.general_ledger import toggle_debit_credit_if_negative @@ -1130,40 +1139,50 @@ def repost_gle_for_stock_vouchers( if not stock_vouchers: return - def _delete_gl_entries(voucher_type, voucher_no): - frappe.db.sql( - """delete from `tabGL Entry` - where voucher_type=%s and voucher_no=%s""", - (voucher_type, voucher_no), - ) - - stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers) - if not warehouse_account: warehouse_account = get_warehouse_account_map(company) + stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers) + if repost_doc and repost_doc.gl_reposting_index: + # Restore progress + stock_vouchers = stock_vouchers[cint(repost_doc.gl_reposting_index) :] + precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit")) or 2 - gle = get_voucherwise_gl_entries(stock_vouchers, posting_date) - for idx, (voucher_type, voucher_no) in enumerate(stock_vouchers): - existing_gle = gle.get((voucher_type, voucher_no), []) - voucher_obj = frappe.get_doc(voucher_type, voucher_no) - # Some transactions post credit as negative debit, this is handled while posting GLE - # but while comparing we need to make sure it's flipped so comparisons are accurate - expected_gle = toggle_debit_credit_if_negative(voucher_obj.get_gl_entries(warehouse_account)) - if expected_gle: - if not existing_gle or not compare_existing_and_expected_gle( - existing_gle, expected_gle, precision - ): - _delete_gl_entries(voucher_type, voucher_no) - voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True) - else: - _delete_gl_entries(voucher_type, voucher_no) + stock_vouchers_iterator = iter(stock_vouchers) - if idx % 20 == 0: - # Commit every 20 documents to avoid losing progress - # and reducing memory usage - frappe.db.commit() + while stock_vouchers_chunk := list(itertools.islice(stock_vouchers_iterator, GL_REPOSTING_CHUNK)): + gle = get_voucherwise_gl_entries(stock_vouchers_chunk, posting_date) + + for voucher_type, voucher_no in stock_vouchers_chunk: + existing_gle = gle.get((voucher_type, voucher_no), []) + voucher_obj = frappe.get_doc(voucher_type, voucher_no) + # Some transactions post credit as negative debit, this is handled while posting GLE + # but while comparing we need to make sure it's flipped so comparisons are accurate + expected_gle = toggle_debit_credit_if_negative(voucher_obj.get_gl_entries(warehouse_account)) + if expected_gle: + if not existing_gle or not compare_existing_and_expected_gle( + existing_gle, expected_gle, precision + ): + _delete_gl_entries(voucher_type, voucher_no) + voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True) + else: + _delete_gl_entries(voucher_type, voucher_no) + frappe.db.commit() + + if repost_doc: + repost_doc.db_set( + "gl_reposting_index", + cint(repost_doc.gl_reposting_index) + GL_REPOSTING_CHUNK, + ) + + +def _delete_gl_entries(voucher_type, voucher_no): + frappe.db.sql( + """delete from `tabGL Entry` + where voucher_type=%s and voucher_no=%s""", + (voucher_type, voucher_no), + ) def sort_stock_vouchers_by_posting_date( @@ -1177,6 +1196,9 @@ def sort_stock_vouchers_by_posting_date( .select(sle.voucher_type, sle.voucher_no, sle.posting_date, sle.posting_time, sle.creation) .where((sle.is_cancelled == 0) & (sle.voucher_no.isin(voucher_nos))) .groupby(sle.voucher_type, sle.voucher_no) + .orderby(sle.posting_date) + .orderby(sle.posting_time) + .orderby(sle.creation) ).run(as_dict=True) sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles] diff --git a/erpnext/accounts/workspace/accounting/accounting.json b/erpnext/accounts/workspace/accounting/accounting.json index a456c7fb57..61f6225459 100644 --- a/erpnext/accounts/workspace/accounting/accounting.json +++ b/erpnext/accounts/workspace/accounting/accounting.json @@ -504,18 +504,6 @@ "onboard": 0, "type": "Link" }, - { - "dependencies": "GL Entry", - "hidden": 0, - "is_query_report": 1, - "label": "DATEV Export", - "link_count": 0, - "link_to": "DATEV", - "link_type": "Report", - "onboard": 0, - "only_for": "Germany", - "type": "Link" - }, { "dependencies": "GL Entry", "hidden": 0, @@ -1024,16 +1012,16 @@ "type": "Link" }, { - "dependencies": "Cost Center", - "hidden": 0, - "is_query_report": 0, - "label": "Cost Center Allocation", - "link_count": 0, - "link_to": "Cost Center Allocation", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, + "dependencies": "Cost Center", + "hidden": 0, + "is_query_report": 0, + "label": "Cost Center Allocation", + "link_count": 0, + "link_to": "Cost Center Allocation", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "dependencies": "Cost Center", "hidden": 0, @@ -1235,13 +1223,14 @@ "type": "Link" } ], - "modified": "2022-01-13 17:25:09.835345", + "modified": "2022-06-10 15:49:42.990860", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting", "owner": "Administrator", "parent_page": "", "public": 1, + "quick_lists": [], "restrict_to_domain": "", "roles": [], "sequence_id": 2.0, diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 1a7f2dd5d9..d732b755fe 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -330,7 +330,7 @@ class TestPurchaseOrder(FrappeTestCase): else: # update valid from frappe.db.sql( - """UPDATE `tabItem Tax` set valid_from = CURDATE() + """UPDATE `tabItem Tax` set valid_from = CURRENT_DATE where parent = %(item)s and item_tax_template = %(tax)s""", {"item": item, "tax": tax_template}, ) 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 d39aec19ba..67affe770f 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -285,7 +285,7 @@ def get_supplier_contacts(doctype, txt, searchfield, start, page_len, filters): """select `tabContact`.name from `tabContact`, `tabDynamic Link` where `tabDynamic Link`.link_doctype = 'Supplier' and (`tabDynamic Link`.link_name=%(name)s and `tabDynamic Link`.link_name like %(txt)s) and `tabContact`.name = `tabDynamic Link`.parent - limit %(start)s, %(page_len)s""", + limit %(page_len)s offset %(start)s""", {"start": start, "page_len": page_len, "txt": "%%%s%%" % txt, "name": filters.get("supplier")}, ) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index eeb5a7f1a9..a725f674c9 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -33,7 +33,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): if(locate(%(_txt)s, employee_name), locate(%(_txt)s, employee_name), 99999), idx desc, name, employee_name - limit %(start)s, %(page_len)s""".format( + limit %(page_len)s offset %(start)s""".format( **{ "fields": ", ".join(fields), "key": searchfield, @@ -65,7 +65,7 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters): if(locate(%(_txt)s, company_name), locate(%(_txt)s, company_name), 99999), idx desc, name, lead_name - limit %(start)s, %(page_len)s""".format( + limit %(page_len)s offset %(start)s""".format( **{"fields": ", ".join(fields), "key": searchfield, "mcond": get_match_cond(doctype)} ), {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, @@ -100,7 +100,7 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters): if(locate(%(_txt)s, customer_name), locate(%(_txt)s, customer_name), 99999), idx desc, name, customer_name - limit %(start)s, %(page_len)s""".format( + limit %(page_len)s offset %(start)s""".format( **{ "fields": ", ".join(fields), "scond": searchfields, @@ -137,7 +137,7 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters): if(locate(%(_txt)s, supplier_name), locate(%(_txt)s, supplier_name), 99999), idx desc, name, supplier_name - limit %(start)s, %(page_len)s """.format( + limit %(page_len)s offset %(start)s""".format( **{"field": ", ".join(fields), "key": searchfield, "mcond": get_match_cond(doctype)} ), {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, @@ -167,7 +167,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters): AND `{searchfield}` LIKE %(txt)s {mcond} ORDER BY idx DESC, name - LIMIT %(offset)s, %(limit)s + LIMIT %(limit)s offset %(offset)s """.format( account_type_condition=account_type_condition, searchfield=searchfield, @@ -351,7 +351,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), idx desc, `tabProject`.name asc - limit {start}, {page_len}""".format( + limit {page_len} offset {start}""".format( fields=", ".join(["`tabProject`.{0}".format(f) for f in fields]), cond=cond, scond=searchfields, @@ -383,7 +383,7 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, and return_against in (select name from `tabDelivery Note` where per_billed < 100) ) ) - %(mcond)s order by `tabDelivery Note`.`%(key)s` asc limit %(start)s, %(page_len)s + %(mcond)s order by `tabDelivery Note`.`%(key)s` asc limit %(page_len)s offset %(start)s """ % { "fields": ", ".join(["`tabDelivery Note`.{0}".format(f) for f in fields]), @@ -456,7 +456,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): {match_conditions} group by batch_no {having_clause} order by batch.expiry_date, sle.batch_no desc - limit %(start)s, %(page_len)s""".format( + limit %(page_len)s offset %(start)s""".format( search_columns=search_columns, cond=cond, match_conditions=get_match_cond(doctype), @@ -483,7 +483,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): {match_conditions} order by expiry_date, name desc - limit %(start)s, %(page_len)s""".format( + limit %(page_len)s offset %(start)s""".format( cond, search_columns=search_columns, search_cond=search_cond, @@ -662,7 +662,7 @@ def warehouse_query(doctype, txt, searchfield, start, page_len, filters): {fcond} {mcond} order by ifnull(`tabBin`.actual_qty, 0) desc limit - {start}, {page_len} + {page_len} offset {start} """.format( bin_conditions=get_filters_cond( doctype, filter_dict.get("Bin"), bin_conditions, ignore_permissions=True @@ -691,7 +691,7 @@ def get_doctype_wise_filters(filters): def get_batch_numbers(doctype, txt, searchfield, start, page_len, filters): query = """select batch_id from `tabBatch` where disabled = 0 - and (expiry_date >= CURDATE() or expiry_date IS NULL) + and (expiry_date >= CURRENT_DATE or expiry_date IS NULL) and name like {txt}""".format( txt=frappe.db.escape("%{0}%".format(txt)) ) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 3c0a10e086..517e080c97 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -35,7 +35,8 @@ status_map = { ["Draft", None], ["Open", "eval:self.docstatus==1"], ["Lost", "eval:self.status=='Lost'"], - ["Ordered", "has_sales_order"], + ["Partially Ordered", "is_partially_ordered"], + ["Ordered", "is_fully_ordered"], ["Cancelled", "eval:self.docstatus==2"], ], "Sales Order": [ diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index b590177562..c70a4f61b8 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -8,7 +8,8 @@ import frappe from frappe import _ from frappe.email.inbox import link_communication_to_document from frappe.model.mapper import get_mapped_doc -from frappe.query_builder import DocType +from frappe.query_builder import DocType, Interval +from frappe.query_builder.functions import Now from frappe.utils import cint, flt, get_fullname from erpnext.crm.utils import add_link_in_communication, copy_comments @@ -398,15 +399,17 @@ def auto_close_opportunity(): frappe.db.get_single_value("CRM Settings", "close_opportunity_after_days") or 15 ) - opportunities = frappe.db.sql( - """ select name from tabOpportunity where status='Replied' and - modified qty and docstatus = 1 - order by name limit {1}, {2}""".format( + order by name limit {2} offset {1}""".format( cond, start, page_len ), {"name": "%%%s%%" % txt}, diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json index 9829a96e09..549f5afc70 100644 --- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json +++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order Summary\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", "creation": "2020-03-02 17:11:37.032604", "docstatus": 0, "doctype": "Workspace", @@ -402,7 +402,7 @@ "type": "Link" } ], - "modified": "2022-05-31 22:08:19.408223", + "modified": "2022-06-15 15:18:57.062935", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing", @@ -415,39 +415,35 @@ "sequence_id": 17.0, "shortcuts": [ { - "color": "Green", - "format": "{} Active", - "label": "Item", - "link_to": "Item", - "restrict_to_domain": "Manufacturing", - "stats_filter": "{\n \"disabled\": 0\n}", - "type": "DocType" - }, - { - "color": "Green", - "format": "{} Active", + "color": "Grey", + "doc_view": "List", "label": "BOM", "link_to": "BOM", - "restrict_to_domain": "Manufacturing", - "stats_filter": "{\n \"is_active\": 1\n}", + "stats_filter": "{\"is_active\":[\"=\",1]}", "type": "DocType" }, { - "color": "Yellow", - "format": "{} Open", - "label": "Work Order", - "link_to": "Work Order", - "restrict_to_domain": "Manufacturing", - "stats_filter": "{ \n \"status\": [\"in\", \n [\"Draft\", \"Not Started\", \"In Process\"]\n ]\n}", - "type": "DocType" - }, - { - "color": "Yellow", - "format": "{} Open", + "color": "Grey", + "doc_view": "List", "label": "Production Plan", "link_to": "Production Plan", - "restrict_to_domain": "Manufacturing", - "stats_filter": "{ \n \"status\": [\"not in\", [\"Completed\"]]\n}", + "stats_filter": "{\"status\":[\"not in\",[\"Closed\",\"Cancelled\",\"Completed\"]]}", + "type": "DocType" + }, + { + "color": "Grey", + "doc_view": "List", + "label": "Work Order", + "link_to": "Work Order", + "stats_filter": "{\"status\":[\"not in\",[\"Closed\",\"Cancelled\",\"Completed\"]]}", + "type": "DocType" + }, + { + "color": "Grey", + "doc_view": "List", + "label": "Job Card", + "link_to": "Job Card", + "stats_filter": "{\"status\":[\"not in\",[\"Cancelled\",\"Completed\",null]]}", "type": "DocType" }, { @@ -455,12 +451,6 @@ "link_to": "Exponential Smoothing Forecasting", "type": "Report" }, - { - "label": "Work Order Summary", - "link_to": "Work Order Summary", - "restrict_to_domain": "Manufacturing", - "type": "Report" - }, { "label": "BOM Stock Report", "link_to": "BOM Stock Report", @@ -470,12 +460,6 @@ "label": "Production Planning Report", "link_to": "Production Planning Report", "type": "Report" - }, - { - "label": "Dashboard", - "link_to": "Manufacturing", - "restrict_to_domain": "Manufacturing", - "type": "Dashboard" } ], "title": "Manufacturing" diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 5a984635fd..318875d2a4 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -339,7 +339,7 @@ erpnext.patches.v14_0.delete_shopify_doctypes erpnext.patches.v14_0.delete_healthcare_doctypes erpnext.patches.v14_0.delete_hub_doctypes erpnext.patches.v14_0.delete_hospitality_doctypes # 20-01-2022 -erpnext.patches.v14_0.delete_agriculture_doctypes +erpnext.patches.v14_0.delete_agriculture_doctypes # 15-06-2022 erpnext.patches.v14_0.delete_education_doctypes erpnext.patches.v14_0.delete_datev_doctypes erpnext.patches.v14_0.rearrange_company_fields @@ -374,3 +374,4 @@ erpnext.patches.v13_0.set_per_billed_in_return_delivery_note execute:frappe.delete_doc("DocType", "Naming Series") erpnext.patches.v13_0.set_payroll_entry_status erpnext.patches.v13_0.job_card_status_on_hold +erpnext.patches.v14_0.migrate_gl_to_payment_ledger diff --git a/erpnext/patches/v14_0/delete_agriculture_doctypes.py b/erpnext/patches/v14_0/delete_agriculture_doctypes.py index e0b12a2579..8ec0c33090 100644 --- a/erpnext/patches/v14_0/delete_agriculture_doctypes.py +++ b/erpnext/patches/v14_0/delete_agriculture_doctypes.py @@ -2,6 +2,9 @@ import frappe def execute(): + if "agriculture" in frappe.get_installed_apps(): + return + frappe.delete_doc("Module Def", "Agriculture", ignore_missing=True, force=True) frappe.delete_doc("Workspace", "Agriculture", ignore_missing=True, force=True) @@ -19,3 +22,5 @@ def execute(): doctypes = frappe.get_all("DocType", {"module": "agriculture", "custom": 0}, pluck="name") for doctype in doctypes: frappe.delete_doc("DocType", doctype, ignore_missing=True) + + frappe.delete_doc("Module Def", "Agriculture", ignore_missing=True, force=True) diff --git a/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py b/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py index c2267aa9af..1e0d20d059 100644 --- a/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py +++ b/erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py @@ -1,11 +1,13 @@ import frappe from frappe import qb +from frappe.query_builder import Case +from frappe.query_builder.custom import ConstantColumn +from frappe.query_builder.functions import IfNull from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_dimensions, make_dimension_in_accounting_doctypes, ) -from erpnext.accounts.utils import create_payment_ledger_entry def create_accounting_dimension_fields(): @@ -15,24 +17,119 @@ def create_accounting_dimension_fields(): make_dimension_in_accounting_doctypes(dimension, ["Payment Ledger Entry"]) -def execute(): - # create accounting dimension fields in Payment Ledger - create_accounting_dimension_fields() +def generate_name_for_payment_ledger_entries(gl_entries): + for index, entry in enumerate(gl_entries, 1): + entry.name = index + + +def get_columns(): + columns = [ + "name", + "creation", + "modified", + "modified_by", + "owner", + "docstatus", + "posting_date", + "account_type", + "account", + "party_type", + "party", + "voucher_type", + "voucher_no", + "against_voucher_type", + "against_voucher_no", + "amount", + "amount_in_account_currency", + "account_currency", + "company", + "cost_center", + "due_date", + "finance_book", + ] + + dimensions_and_defaults = get_dimensions() + if dimensions_and_defaults: + for dimension in dimensions_and_defaults[0]: + columns.append(dimension.fieldname) + + return columns + + +def build_insert_query(): + ple = qb.DocType("Payment Ledger Entry") + columns = get_columns() + insert_query = qb.into(ple) + + # build 'insert' columns in query + insert_query = insert_query.columns(tuple(columns)) + + return insert_query + + +def insert_chunk_into_payment_ledger(insert_query, gl_entries): + if gl_entries: + columns = get_columns() + + # build tuple of data with same column order + for entry in gl_entries: + data = () + for column in columns: + data += (entry[column],) + insert_query = insert_query.insert(data) + insert_query.run() + + +def execute(): + if frappe.reload_doc("accounts", "doctype", "payment_ledger_entry"): + # create accounting dimension fields in Payment Ledger + create_accounting_dimension_fields() + + gl = qb.DocType("GL Entry") + account = qb.DocType("Account") - gl = qb.DocType("GL Entry") - accounts = frappe.db.get_list( - "Account", "name", filters={"account_type": ["in", ["Receivable", "Payable"]]}, as_list=True - ) - gl_entries = [] - if accounts: - # get all gl entries on receivable/payable accounts gl_entries = ( qb.from_(gl) - .select("*") - .where(gl.account.isin(accounts)) + .inner_join(account) + .on((gl.account == account.name) & (account.account_type.isin(["Receivable", "Payable"]))) + .select( + gl.star, + ConstantColumn(1).as_("docstatus"), + account.account_type.as_("account_type"), + IfNull(gl.against_voucher_type, gl.voucher_type).as_("against_voucher_type"), + IfNull(gl.against_voucher, gl.voucher_no).as_("against_voucher_no"), + # convert debit/credit to amount + Case() + .when(account.account_type == "Receivable", gl.debit - gl.credit) + .else_(gl.credit - gl.debit) + .as_("amount"), + # convert debit/credit in account currency to amount in account currency + Case() + .when( + account.account_type == "Receivable", + gl.debit_in_account_currency - gl.credit_in_account_currency, + ) + .else_(gl.credit_in_account_currency - gl.debit_in_account_currency) + .as_("amount_in_account_currency"), + ) .where(gl.is_cancelled == 0) + .orderby(gl.creation) .run(as_dict=True) ) - if gl_entries: - # create payment ledger entries for the accounts receivable/payable - create_payment_ledger_entry(gl_entries, 0) + + # primary key(name) for payment ledger records + generate_name_for_payment_ledger_entries(gl_entries) + + # split data into chunks + chunk_size = 1000 + try: + for i in range(0, len(gl_entries), chunk_size): + insert_query = build_insert_query() + insert_chunk_into_payment_ledger(insert_query, gl_entries[i : i + chunk_size]) + frappe.db.commit() + except Exception as err: + frappe.db.rollback() + ple = qb.DocType("Payment Ledger Entry") + qb.from_(ple).delete().where(ple.docstatus >= 0).run() + frappe.db.commit() + raise err diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 620fcadceb..1524fb7c9e 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -970,7 +970,7 @@ def get_payroll_entries_for_jv(doctype, txt, searchfield, start, page_len, filte and name not in (select reference_name from `tabJournal Entry Account` where reference_type="Payroll Entry") - order by name limit %(start)s, %(page_len)s""".format( + order by name limit %(page_len)s offset %(start)s""".format( key=searchfield ), {"txt": "%%%s%%" % txt, "start": start, "page_len": page_len}, @@ -1039,7 +1039,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): if(locate(%(_txt)s, employee_name), locate(%(_txt)s, employee_name), 99999), idx desc, name, employee_name - limit %(start)s, %(page_len)s""".format( + limit %(page_len)s offset %(start)s""".format( **{ "key": searchfield, "fcond": get_filters_cond(doctype, filters, conditions), diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 8a8e1d1bf5..c613fe633d 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -391,7 +391,7 @@ def get_users_for_project(doctype, txt, searchfield, start, page_len, filters): if(locate(%(_txt)s, full_name), locate(%(_txt)s, full_name), 99999), idx desc, name, full_name - limit %(start)s, %(page_len)s""".format( + limit %(page_len)s offset %(start)s""".format( **{ "key": searchfield, "fcond": get_filters_cond(doctype, filters, conditions), diff --git a/erpnext/projects/doctype/project_update/project_update.py b/erpnext/projects/doctype/project_update/project_update.py index 5a29fb6c33..175f787a30 100644 --- a/erpnext/projects/doctype/project_update/project_update.py +++ b/erpnext/projects/doctype/project_update/project_update.py @@ -28,7 +28,7 @@ def daily_reminder(): for drafts in draft: number_of_drafts = drafts[0] update = frappe.db.sql( - """SELECT name,date,time,progress,progress_details FROM `tabProject Update` WHERE `tabProject Update`.project = %s AND date = DATE_ADD(CURDATE(), INTERVAL -1 DAY);""", + """SELECT name,date,time,progress,progress_details FROM `tabProject Update` WHERE `tabProject Update`.project = %s AND date = DATE_ADD(CURRENT_DATE, INTERVAL -1 DAY);""", project_name, ) email_sending(project_name, frequency, date_start, date_end, progress, number_of_drafts, update) @@ -39,7 +39,7 @@ def email_sending( ): holiday = frappe.db.sql( - """SELECT holiday_date FROM `tabHoliday` where holiday_date = CURDATE();""" + """SELECT holiday_date FROM `tabHoliday` where holiday_date = CURRENT_DATE;""" ) msg = ( "

Project Name: " diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 4575fb544c..0e409fce9c 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -288,7 +288,7 @@ def get_project(doctype, txt, searchfield, start, page_len, filters): %(mcond)s {search_condition} order by name - limit %(start)s, %(page_len)s""".format( + limit %(page_len)s offset %(start)s""".format( search_columns=search_columns, search_condition=search_cond ), { diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index 2ef966b319..88d5beef17 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -328,7 +328,7 @@ def get_timesheet(doctype, txt, searchfield, start, page_len, filters): ts.status in ('Submitted', 'Payslip') and tsd.parent = ts.name and tsd.docstatus = 1 and ts.total_billable_amount > 0 and tsd.parent LIKE %(txt)s {condition} - order by tsd.parent limit %(start)s, %(page_len)s""".format( + order by tsd.parent limit %(page_len)s offset %(start)s""".format( condition=condition ), { @@ -515,7 +515,7 @@ def get_timesheets_list( tsd.project IN %(projects)s ) ORDER BY `end_date` ASC - LIMIT {0}, {1} + LIMIT {1} offset {0} """.format( limit_start, limit_page_length ), diff --git a/erpnext/projects/utils.py b/erpnext/projects/utils.py index 000ea66275..3cc4da4f07 100644 --- a/erpnext/projects/utils.py +++ b/erpnext/projects/utils.py @@ -25,7 +25,7 @@ def query_task(doctype, txt, searchfield, start, page_len, filters): case when `%s` like %s then 0 else 1 end, `%s`, subject - limit %s, %s""" + limit %s offset %s""" % (searchfield, "%s", "%s", match_conditions, "%s", searchfield, "%s", searchfield, "%s", "%s"), - (search_string, search_string, order_by_string, order_by_string, start, page_len), + (search_string, search_string, order_by_string, order_by_string, page_len, start), ) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index ee48ccb24a..0262469571 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -287,7 +287,7 @@ def get_regional_address_details(party_details, doctype, company): return party_details if ( - doctype in ("Sales Invoice", "Delivery Note", "Sales Order") + doctype in ("Sales Invoice", "Delivery Note", "Sales Order", "Quotation") and party_details.company_gstin and party_details.company_gstin[:2] != party_details.place_of_supply[:2] ) or ( diff --git a/erpnext/regional/report/irs_1099/irs_1099.py b/erpnext/regional/report/irs_1099/irs_1099.py index 92aeb5ee6f..0f578be175 100644 --- a/erpnext/regional/report/irs_1099/irs_1099.py +++ b/erpnext/regional/report/irs_1099/irs_1099.py @@ -10,7 +10,7 @@ from frappe.utils.data import fmt_money from frappe.utils.jinja import render_template from frappe.utils.pdf import get_pdf from frappe.utils.print_format import read_multi_pdf -from PyPDF2 import PdfFileWriter +from PyPDF2 import PdfWriter from erpnext.accounts.utils import get_fiscal_year @@ -106,7 +106,7 @@ def irs_1099_print(filters): columns, data = execute(filters) template = frappe.get_doc("Print Format", "IRS 1099 Form").html - output = PdfFileWriter() + output = PdfWriter() for row in data: row["fiscal_year"] = fiscal_year diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py index 575b956686..ac83c0f046 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/product_bundle.py @@ -78,7 +78,7 @@ def get_new_item_code(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql( """select name, item_name, description from tabItem where is_stock_item=0 and name not in (select name from `tabProduct Bundle`) - and %s like %s %s limit %s, %s""" + and %s like %s %s limit %s offset %s""" % (searchfield, "%s", get_match_cond(doctype), "%s", "%s"), - ("%%%s%%" % txt, start, page_len), + ("%%%s%%" % txt, page_len, start), ) diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 34e9a52e11..70ae085051 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -20,6 +20,20 @@ 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 + } + }; + }); }, refresh: function(frm) { @@ -70,7 +84,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. } } - if(doc.docstatus == 1 && doc.status!=='Lost') { + if(doc.docstatus == 1 && !(['Lost', 'Ordered']).includes(doc.status)) { if(!doc.valid_till || frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) { cur_frm.add_custom_button(__('Sales Order'), cur_frm.cscript['Make Sales Order'], __('Create')); diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index 75443abe49..bb2f95dd17 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -296,7 +296,7 @@ "read_only": 1 }, { - "depends_on": "eval:doc.quotaion_to=='Customer' && doc.party_name", + "depends_on": "eval:doc.quotation_to=='Customer' && doc.party_name", "fieldname": "col_break98", "fieldtype": "Column Break", "width": "50%" @@ -316,7 +316,7 @@ "read_only": 1 }, { - "depends_on": "eval:doc.quotaion_to=='Customer' && doc.party_name", + "depends_on": "eval:doc.quotation_to=='Customer' && doc.party_name", "fieldname": "customer_group", "fieldtype": "Link", "hidden": 1, @@ -897,7 +897,7 @@ "no_copy": 1, "oldfieldname": "status", "oldfieldtype": "Select", - "options": "Draft\nOpen\nReplied\nOrdered\nLost\nCancelled\nExpired", + "options": "Draft\nOpen\nReplied\nPartially Ordered\nOrdered\nLost\nCancelled\nExpired", "print_hide": 1, "read_only": 1, "reqd": 1 @@ -986,7 +986,7 @@ "idx": 82, "is_submittable": 1, "links": [], - "modified": "2022-04-07 11:01:31.157084", + "modified": "2022-06-11 20:35:32.635804", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", @@ -1084,4 +1084,4 @@ "states": [], "timeline_field": "party_name", "title_field": "title" -} \ No newline at end of file +} diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 548813ddef..d5fd946bde 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -70,8 +70,32 @@ class Quotation(SellingController): title=_("Unpublished Item"), ) - def has_sales_order(self): - return frappe.db.get_value("Sales Order Item", {"prevdoc_docname": self.name, "docstatus": 1}) + def get_ordered_status(self): + ordered_items = frappe._dict( + frappe.db.get_all( + "Sales Order Item", + {"prevdoc_docname": self.name, "docstatus": 1}, + ["item_code", "sum(qty)"], + group_by="item_code", + as_list=1, + ) + ) + + status = "Open" + if ordered_items: + status = "Ordered" + + for item in self.get("items"): + if item.qty > ordered_items.get(item.item_code, 0.0): + status = "Partially Ordered" + + return status + + def is_fully_ordered(self): + return self.get_ordered_status() == "Ordered" + + def is_partially_ordered(self): + return self.get_ordered_status() == "Partially Ordered" def update_lead(self): if self.quotation_to == "Lead" and self.party_name: diff --git a/erpnext/selling/doctype/quotation/quotation_list.js b/erpnext/selling/doctype/quotation/quotation_list.js index 4c8f9c4f84..32fce1f2ad 100644 --- a/erpnext/selling/doctype/quotation/quotation_list.js +++ b/erpnext/selling/doctype/quotation/quotation_list.js @@ -25,6 +25,8 @@ frappe.listview_settings['Quotation'] = { get_indicator: function(doc) { if(doc.status==="Open") { return [__("Open"), "orange", "status,=,Open"]; + } else if (doc.status==="Partially Ordered") { + return [__("Partially Ordered"), "yellow", "status,=,Partially Ordered"]; } else if(doc.status==="Ordered") { return [__("Ordered"), "green", "status,=,Ordered"]; } else if(doc.status==="Lost") { diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 7522e92a8a..8c03cb5b41 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -25,6 +25,7 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import ( from erpnext.selling.doctype.customer.customer import check_credit_limit from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.stock.doctype.item.item import get_item_defaults +from erpnext.stock.get_item_details import get_default_bom from erpnext.stock.stock_balance import get_reserved_qty, update_bin_qty form_grid_templates = {"items": "templates/form_grid/item_grid.html"} @@ -423,8 +424,9 @@ class SalesOrder(SellingController): for table in [self.items, self.packed_items]: for i in table: - bom = get_default_bom_item(i.item_code) + bom = get_default_bom(i.item_code) stock_qty = i.qty if i.doctype == "Packed Item" else i.stock_qty + if not for_raw_material_request: total_work_order_qty = flt( frappe.db.sql( @@ -438,32 +440,19 @@ class SalesOrder(SellingController): pending_qty = stock_qty if pending_qty and i.item_code not in product_bundle_parents: - if bom: - items.append( - dict( - name=i.name, - item_code=i.item_code, - description=i.description, - bom=bom, - warehouse=i.warehouse, - pending_qty=pending_qty, - required_qty=pending_qty if for_raw_material_request else 0, - sales_order_item=i.name, - ) - ) - else: - items.append( - dict( - name=i.name, - item_code=i.item_code, - description=i.description, - bom="", - warehouse=i.warehouse, - pending_qty=pending_qty, - required_qty=pending_qty if for_raw_material_request else 0, - sales_order_item=i.name, - ) + items.append( + dict( + name=i.name, + item_code=i.item_code, + description=i.description, + bom=bom or "", + warehouse=i.warehouse, + pending_qty=pending_qty, + required_qty=pending_qty if for_raw_material_request else 0, + sales_order_item=i.name, ) + ) + return items def on_recurring(self, reference_doc, auto_repeat_doc): @@ -1167,13 +1156,6 @@ def update_status(status, name): so.update_status(status) -def get_default_bom_item(item_code): - bom = frappe.get_all("BOM", dict(item=item_code, is_active=True), order_by="is_default desc") - bom = bom[0].name if bom else None - - return bom - - @frappe.whitelist() def make_raw_material_request(items, company, sales_order, project=None): if not frappe.has_permission("Sales Order", "write"): diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 96308f0bee..45868fb329 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -644,7 +644,7 @@ class TestSalesOrder(FrappeTestCase): else: # update valid from frappe.db.sql( - """UPDATE `tabItem Tax` set valid_from = CURDATE() + """UPDATE `tabItem Tax` set valid_from = CURRENT_DATE where parent = %(item)s and item_tax_template = %(tax)s""", {"item": item, "tax": tax_template}, ) @@ -1380,6 +1380,59 @@ class TestSalesOrder(FrappeTestCase): except Exception: self.fail("Can not cancel sales order with linked cancelled payment entry") + def test_work_order_pop_up_from_sales_order(self): + "Test `get_work_order_items` in Sales Order picks the right BOM for items to manufacture." + + from erpnext.controllers.item_variant import create_variant + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + + make_item( # template item + "Test-WO-Tshirt", + { + "has_variant": 1, + "variant_based_on": "Item Attribute", + "attributes": [{"attribute": "Test Colour"}], + }, + ) + make_item("Test-RM-Cotton") # RM for BOM + + for colour in ( + "Red", + "Green", + ): + variant = create_variant("Test-WO-Tshirt", {"Test Colour": colour}) + variant.save() + + template_bom = make_bom(item="Test-WO-Tshirt", rate=100, raw_materials=["Test-RM-Cotton"]) + red_var_bom = make_bom(item="Test-WO-Tshirt-R", rate=100, raw_materials=["Test-RM-Cotton"]) + + so = make_sales_order( + **{ + "item_list": [ + { + "item_code": "Test-WO-Tshirt-R", + "qty": 1, + "rate": 1000, + "warehouse": "_Test Warehouse - _TC", + }, + { + "item_code": "Test-WO-Tshirt-G", + "qty": 1, + "rate": 1000, + "warehouse": "_Test Warehouse - _TC", + }, + ] + } + ) + wo_items = so.get_work_order_items() + + self.assertEqual(wo_items[0].get("item_code"), "Test-WO-Tshirt-R") + self.assertEqual(wo_items[0].get("bom"), red_var_bom.name) + + # Must pick Template Item BOM for Test-WO-Tshirt-G as it has no BOM + self.assertEqual(wo_items[1].get("item_code"), "Test-WO-Tshirt-G") + self.assertEqual(wo_items[1].get("bom"), template_bom.name) + def test_request_for_raw_materials(self): item = make_item( "_Test Finished Item", 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 99afe813cb..13d5069ea6 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -107,7 +107,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te ORDER BY item.name asc LIMIT - {start}, {page_length}""".format( + {page_length} offset {start}""".format( start=start, page_length=page_length, lft=lft, @@ -204,7 +204,7 @@ def item_group_query(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql( """ select distinct name from `tabItem Group` - where {condition} and (name like %(txt)s) limit {start}, {page_len}""".format( + where {condition} and (name like %(txt)s) limit {page_len} offset {start}""".format( condition=cond, start=start, page_len=page_len ), {"txt": "%%%s%%" % txt}, diff --git a/erpnext/selling/report/inactive_customers/inactive_customers.py b/erpnext/selling/report/inactive_customers/inactive_customers.py index 1b337fc495..a166085327 100644 --- a/erpnext/selling/report/inactive_customers/inactive_customers.py +++ b/erpnext/selling/report/inactive_customers/inactive_customers.py @@ -31,13 +31,13 @@ def execute(filters=None): def get_sales_details(doctype): cond = """sum(so.base_net_total) as 'total_order_considered', max(so.posting_date) as 'last_order_date', - DATEDIFF(CURDATE(), max(so.posting_date)) as 'days_since_last_order' """ + DATEDIFF(CURRENT_DATE, max(so.posting_date)) as 'days_since_last_order' """ if doctype == "Sales Order": cond = """sum(if(so.status = "Stopped", so.base_net_total * so.per_delivered/100, so.base_net_total)) as 'total_order_considered', max(so.transaction_date) as 'last_order_date', - DATEDIFF(CURDATE(), max(so.transaction_date)) as 'days_since_last_order'""" + DATEDIFF(CURRENT_DATE, max(so.transaction_date)) as 'days_since_last_order'""" return frappe.db.sql( """select 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 cc61594af4..720aa41982 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -64,7 +64,7 @@ def get_data(conditions, filters): soi.delivery_date as delivery_date, so.name as sales_order, so.status, so.customer, soi.item_code, - DATEDIFF(CURDATE(), soi.delivery_date) as delay_days, + DATEDIFF(CURRENT_DATE, soi.delivery_date) as delay_days, IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay, soi.qty, soi.delivered_qty, (soi.qty - soi.delivered_qty) AS pending_qty, diff --git a/erpnext/setup/doctype/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py index cdfea7764f..42ba6ce394 100644 --- a/erpnext/setup/doctype/email_digest/email_digest.py +++ b/erpnext/setup/doctype/email_digest/email_digest.py @@ -854,7 +854,7 @@ class EmailDigest(Document): sql_po = """select {fields} from `tabPurchase Order Item` left join `tabPurchase Order` on `tabPurchase Order`.name = `tabPurchase Order Item`.parent - where status<>'Closed' and `tabPurchase Order Item`.docstatus=1 and curdate() > `tabPurchase Order Item`.schedule_date + where status<>'Closed' and `tabPurchase Order Item`.docstatus=1 and CURRENT_DATE > `tabPurchase Order Item`.schedule_date and received_qty < qty order by `tabPurchase Order Item`.parent DESC, `tabPurchase Order Item`.schedule_date DESC""".format( fields=fields_po @@ -862,7 +862,7 @@ class EmailDigest(Document): sql_poi = """select {fields} from `tabPurchase Order Item` left join `tabPurchase Order` on `tabPurchase Order`.name = `tabPurchase Order Item`.parent - where status<>'Closed' and `tabPurchase Order Item`.docstatus=1 and curdate() > `tabPurchase Order Item`.schedule_date + where status<>'Closed' and `tabPurchase Order Item`.docstatus=1 and CURRENT_DATE > `tabPurchase Order Item`.schedule_date and received_qty < qty order by `tabPurchase Order Item`.idx""".format( fields=fields_poi ) diff --git a/erpnext/setup/doctype/party_type/party_type.py b/erpnext/setup/doctype/party_type/party_type.py index d07ab08450..cf7cba8452 100644 --- a/erpnext/setup/doctype/party_type/party_type.py +++ b/erpnext/setup/doctype/party_type/party_type.py @@ -21,7 +21,7 @@ def get_party_type(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql( """select name from `tabParty Type` where `{key}` LIKE %(txt)s {cond} - order by name limit %(start)s, %(page_len)s""".format( + order by name limit %(page_len)s offset %(start)s""".format( key=searchfield, cond=cond ), {"txt": "%" + txt + "%", "start": start, "page_len": page_len}, diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 559883f224..52854a0f01 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -335,7 +335,7 @@ def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None): on (`tabBatch`.batch_id = `tabStock Ledger Entry`.batch_no ) where `tabStock Ledger Entry`.item_code = %s and `tabStock Ledger Entry`.warehouse = %s and `tabStock Ledger Entry`.is_cancelled = 0 - and (`tabBatch`.expiry_date >= CURDATE() or `tabBatch`.expiry_date IS NULL) {0} + and (`tabBatch`.expiry_date >= CURRENT_DATE or `tabBatch`.expiry_date IS NULL) {0} group by batch_id order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC """.format( diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 2f6d4fb783..76cb31dc42 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -14,7 +14,6 @@ "details", "naming_series", "item_code", - "variant_of", "item_name", "item_group", "stock_uom", @@ -22,6 +21,7 @@ "disabled", "allow_alternative_item", "is_stock_item", + "has_variants", "include_item_in_manufacturing", "opening_stock", "valuation_rate", @@ -66,7 +66,7 @@ "has_serial_no", "serial_no_series", "variants_section", - "has_variants", + "variant_of", "variant_based_on", "attributes", "accounting", @@ -112,8 +112,8 @@ "quality_inspection_template", "inspection_required_before_delivery", "manufacturing", - "default_bom", "is_sub_contracted_item", + "default_bom", "column_break_74", "customer_code", "default_item_manufacturer", @@ -479,7 +479,7 @@ "collapsible_depends_on": "attributes", "depends_on": "eval:!doc.is_fixed_asset", "fieldname": "variants_section", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Variants" }, { @@ -504,7 +504,8 @@ "fieldname": "attributes", "fieldtype": "Table", "hidden": 1, - "label": "Attributes", + "label": "Variant Attributes", + "mandatory_depends_on": "has_variants", "options": "Item Variant Attribute" }, { @@ -909,7 +910,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "modified": "2022-06-08 11:35:20.094546", + "modified": "2022-06-15 09:02:06.177691", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/item_alternative/item_alternative.py b/erpnext/stock/doctype/item_alternative/item_alternative.py index 0f93bb9e95..fb1a28d846 100644 --- a/erpnext/stock/doctype/item_alternative/item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/item_alternative.py @@ -77,7 +77,7 @@ def get_alternative_items(doctype, txt, searchfield, start, page_len, filters): union (select item_code from `tabItem Alternative` where alternative_item_code = %(item_code)s and item_code like %(txt)s - and two_way = 1) limit {0}, {1} + and two_way = 1) limit {1} offset {0} """.format( start, page_len ), diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index 1af9953451..1ba801134e 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -24,7 +24,7 @@ class TestLandedCostVoucher(FrappeTestCase): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", - supplier_warehouse="Work in Progress - TCP1", + supplier_warehouse="Work In Progress - TCP1", get_multiple_items=True, get_taxes_and_charges=True, ) @@ -195,7 +195,7 @@ class TestLandedCostVoucher(FrappeTestCase): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", - supplier_warehouse="Work in Progress - TCP1", + supplier_warehouse="Work In Progress - TCP1", get_multiple_items=True, get_taxes_and_charges=True, do_not_submit=True, @@ -280,7 +280,7 @@ class TestLandedCostVoucher(FrappeTestCase): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", - supplier_warehouse="Work in Progress - TCP1", + supplier_warehouse="Work In Progress - TCP1", do_not_save=True, ) pr.items[0].cost_center = "Main - TCP1" diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py index e9ccf5fc77..e5b9de8789 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/packing_slip.py @@ -203,7 +203,7 @@ def item_details(doctype, txt, searchfield, start, page_len, filters): where name in ( select item_code FROM `tabDelivery Note Item` where parent= %s) and %s like "%s" %s - limit %s, %s """ + limit %s offset %s """ % ("%s", searchfield, "%s", get_match_cond(doctype), "%s", "%s"), - ((filters or {}).get("delivery_note"), "%%%s%%" % txt, start, page_len), + ((filters or {}).get("delivery_note"), "%%%s%%" % txt, page_len, start), ) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 7fbfa62939..be4f27465e 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -276,7 +276,7 @@ class TestPurchaseReceipt(FrappeTestCase): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", - supplier_warehouse="Work in Progress - TCP1", + supplier_warehouse="Work In Progress - TCP1", get_multiple_items=True, get_taxes_and_charges=True, ) @@ -486,13 +486,13 @@ class TestPurchaseReceipt(FrappeTestCase): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", - supplier_warehouse="Work in Progress - 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", + supplier_warehouse="Work In Progress - TCP1", is_return=1, return_against=pr.name, qty=-2, @@ -573,13 +573,13 @@ class TestPurchaseReceipt(FrappeTestCase): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", - supplier_warehouse="Work in Progress - 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", + supplier_warehouse="Work In Progress - TCP1", is_return=1, return_against=pr.name, qty=-5, @@ -615,7 +615,7 @@ class TestPurchaseReceipt(FrappeTestCase): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", - supplier_warehouse="Work in Progress - TCP1", + supplier_warehouse="Work In Progress - TCP1", qty=2, rejected_qty=2, rejected_warehouse=rejected_warehouse, @@ -624,7 +624,7 @@ class TestPurchaseReceipt(FrappeTestCase): return_pr = make_purchase_receipt( company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", - supplier_warehouse="Work in Progress - TCP1", + supplier_warehouse="Work In Progress - TCP1", is_return=1, return_against=pr.name, qty=-2, @@ -951,7 +951,7 @@ class TestPurchaseReceipt(FrappeTestCase): cost_center=cost_center, company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", - supplier_warehouse="Work in Progress - TCP1", + supplier_warehouse="Work In Progress - TCP1", ) stock_in_hand_account = get_inventory_account(pr.company, pr.get("items")[0].warehouse) @@ -975,7 +975,7 @@ class TestPurchaseReceipt(FrappeTestCase): pr = make_purchase_receipt( company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", - supplier_warehouse="Work in Progress - TCP1", + supplier_warehouse="Work In Progress - TCP1", ) stock_in_hand_account = get_inventory_account(pr.company, pr.get("items")[0].warehouse) diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 331d3e812b..13abfad455 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -232,7 +232,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): FROM `tab{doc}` WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s {qi_condition} {cond} {mcond} - ORDER BY item_code limit {start}, {page_len} + ORDER BY item_code limit {page_len} offset {start} """.format( doc=filters.get("from"), cond=cond, @@ -252,7 +252,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): WHERE name = %(reference_name)s and docstatus < 2 and production_item like %(txt)s {qi_condition} {cond} {mcond} ORDER BY production_item - LIMIT {start}, {page_len} + limit {page_len} offset {start} """.format( doc=filters.get("from"), cond=cond, diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json index 156f77f5ac..e093933829 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json @@ -25,7 +25,8 @@ "items_to_be_repost", "affected_transactions", "distinct_item_and_warehouse", - "current_index" + "current_index", + "gl_reposting_index" ], "fields": [ { @@ -181,12 +182,20 @@ "label": "Affected Transactions", "no_copy": 1, "read_only": 1 + }, + { + "default": "0", + "fieldname": "gl_reposting_index", + "fieldtype": "Int", + "hidden": 1, + "label": "GL reposting index", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-04-18 14:08:08.821602", + "modified": "2022-06-13 12:20:22.182322", "modified_by": "Administrator", "module": "Stock", "name": "Repost Item Valuation", 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 328afc86be..ea24b47a29 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -192,6 +192,7 @@ def repost_gl_entries(doc): directly_dependent_transactions + list(repost_affected_transaction), doc.posting_date, doc.company, + repost_doc=doc, ) diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py index 3184f69aa4..3c74619b46 100644 --- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py @@ -2,10 +2,14 @@ # See license.txt +from unittest.mock import MagicMock, call + import frappe from frappe.tests.utils import FrappeTestCase from frappe.utils import nowdate +from frappe.utils.data import today +from erpnext.accounts.utils import repost_gle_for_stock_vouchers from erpnext.controllers.stock_controller import create_item_wise_repost_entries from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt @@ -193,3 +197,31 @@ class TestRepostItemValuation(FrappeTestCase): [["a", "b"], ["c", "d"]], sorted(frappe.parse_json(frappe.as_json(set([("a", "b"), ("c", "d")])))), ) + + def test_gl_repost_progress(self): + from erpnext.accounts import utils + + # lower numbers to simplify test + orig_chunk_size = utils.GL_REPOSTING_CHUNK + utils.GL_REPOSTING_CHUNK = 1 + self.addCleanup(setattr, utils, "GL_REPOSTING_CHUNK", orig_chunk_size) + + doc = frappe.new_doc("Repost Item Valuation") + doc.db_set = MagicMock() + + vouchers = [] + company = "_Test Company with perpetual inventory" + posting_date = today() + + for _ in range(3): + se = make_stock_entry(company=company, qty=1, rate=2, target="Stores - TCP1") + vouchers.append((se.doctype, se.name)) + + repost_gle_for_stock_vouchers(stock_vouchers=vouchers, posting_date=posting_date, repost_doc=doc) + self.assertIn(call("gl_reposting_index", 1), doc.db_set.mock_calls) + doc.db_set.reset_mock() + + doc.gl_reposting_index = 1 + repost_gle_for_stock_vouchers(stock_vouchers=vouchers, posting_date=posting_date, repost_doc=doc) + + self.assertNotIn(call("gl_reposting_index", 1), doc.db_set.mock_calls) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index f1df54dd6a..a9176a9f12 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1980,23 +1980,30 @@ class StockEntry(StockController): ): # Get PO Supplied Items Details - item_wh = frappe._dict( - frappe.db.sql( - """ - select rm_item_code, reserve_warehouse - from `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup - where po.name = poitemsup.parent - and po.name = %s""", - self.purchase_order, - ) + po_supplied_items = frappe.db.get_all( + "Purchase Order Item Supplied", + filters={"parent": self.purchase_order}, + fields=["name", "rm_item_code", "reserve_warehouse"], ) + # Get Items Supplied in Stock Entries against PO supplied_items = get_supplied_items(self.purchase_order) - for name, item in supplied_items.items(): - frappe.db.set_value("Purchase Order Item Supplied", name, item) - # Update reserved sub contracted quantity in bin based on Supplied Item Details and + for row in po_supplied_items: + key, item = row.name, {} + if not supplied_items.get(key): + # no stock transferred against PO Supplied Items row + item = {"supplied_qty": 0, "returned_qty": 0, "total_supplied_qty": 0} + else: + item = supplied_items.get(key) + + frappe.db.set_value("Purchase Order Item Supplied", row.name, item) + + # RM Item-Reserve Warehouse Dict + item_wh = {x.get("rm_item_code"): x.get("reserve_warehouse") for x in po_supplied_items} + for d in self.get("items"): + # Update reserved sub contracted quantity in bin based on Supplied Item Details and item_code = d.get("original_item") or d.get("item_code") reserve_warehouse = item_wh.get(item_code) if not (reserve_warehouse and item_code): diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index c8d9f5404f..3776a27b35 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -1352,12 +1352,22 @@ def get_price_list_currency_and_exchange_rate(args): @frappe.whitelist() def get_default_bom(item_code=None): - if item_code: - bom = frappe.db.get_value( - "BOM", {"docstatus": 1, "is_default": 1, "is_active": 1, "item": item_code} + def _get_bom(item): + bom = frappe.get_all( + "BOM", dict(item=item, is_active=True, is_default=True, docstatus=1), limit=1 ) - if bom: - return bom + return bom[0].name if bom else None + + if not item_code: + return + + bom_name = _get_bom(item_code) + + template_item = frappe.db.get_value("Item", item_code, "variant_of") + if not bom_name and template_item: + bom_name = _get_bom(template_item) + + return bom_name @frappe.whitelist() diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 08a06b19b4..7f3e0cf4c2 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -11,6 +11,8 @@ from frappe.core.utils import get_parent_doc from frappe.email.inbox import link_communication_to_document from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc +from frappe.query_builder import Interval +from frappe.query_builder.functions import Now from frappe.utils import date_diff, get_datetime, now_datetime, time_diff_in_seconds from frappe.utils.user import is_website_user @@ -190,15 +192,17 @@ def auto_close_tickets(): frappe.db.get_value("Support Settings", "Support Settings", "close_issue_after_days") or 7 ) - issues = frappe.db.sql( - """ select name from tabIssue where status='Replied' and - modified