diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index 1a572d9823e..78c35266542 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -99,7 +99,7 @@ class BankClearance(Document): .where(loan_disbursement.clearance_date.isnull()) .where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account])) .orderby(loan_disbursement.disbursement_date) - .orderby(loan_disbursement.name, frappe.qb.desc) + .orderby(loan_disbursement.name, order=frappe.qb.desc) ).run(as_dict=1) loan_repayment = frappe.qb.DocType("Loan Repayment") @@ -126,7 +126,9 @@ class BankClearance(Document): if frappe.db.has_column("Loan Repayment", "repay_from_salary"): query = query.where((loan_repayment.repay_from_salary == 0)) - query = query.orderby(loan_repayment.posting_date).orderby(loan_repayment.name, frappe.qb.desc) + query = query.orderby(loan_repayment.posting_date).orderby( + loan_repayment.name, order=frappe.qb.desc + ) loan_repayments = query.run(as_dict=True) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 7af41f398ad..763e2e6992c 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -173,8 +173,8 @@ frappe.ui.form.on("Journal Entry", { var update_jv_details = function(doc, r) { $.each(r, function(i, d) { var row = frappe.model.add_child(doc, "Journal Entry Account", "accounts"); - row.account = d.account; - row.balance = d.balance; + frappe.model.set_value(row.doctype, row.name, "account", d.account) + frappe.model.set_value(row.doctype, row.name, "balance", d.balance) }); refresh_field("accounts"); } diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 601fc87a227..52efd33fefa 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -14,6 +14,7 @@ from erpnext.accounts.utils import ( QueryPaymentLedger, get_outstanding_invoices, reconcile_against_document, + update_reference_in_payment_entry, ) from erpnext.controllers.accounts_controller import get_advance_payment_entries @@ -212,6 +213,23 @@ class PaymentReconciliation(Document): inv.currency = entry.get("currency") inv.outstanding_amount = flt(entry.get("outstanding_amount")) + def get_difference_amount(self, allocated_entry): + if allocated_entry.get("reference_type") != "Payment Entry": + return + + dr_or_cr = ( + "credit_in_account_currency" + if erpnext.get_party_account_type(self.party_type) == "Receivable" + else "debit_in_account_currency" + ) + + row = self.get_payment_details(allocated_entry, dr_or_cr) + + doc = frappe.get_doc(allocated_entry.reference_type, allocated_entry.reference_name) + update_reference_in_payment_entry(row, doc, do_not_save=True) + + return doc.difference_amount + @frappe.whitelist() def allocate_entries(self, args): self.validate_entries() @@ -227,12 +245,16 @@ class PaymentReconciliation(Document): res = self.get_allocated_entry(pay, inv, pay["amount"]) inv["outstanding_amount"] = flt(inv.get("outstanding_amount")) - flt(pay.get("amount")) pay["amount"] = 0 + + res.difference_amount = self.get_difference_amount(res) + if pay.get("amount") == 0: entries.append(res) break elif inv.get("outstanding_amount") == 0: entries.append(res) continue + else: break diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json index 2ee356aaf40..2f3516e135a 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.json +++ b/erpnext/accounts/doctype/payment_request/payment_request.json @@ -186,8 +186,10 @@ { "fetch_from": "bank_account.bank", "fieldname": "bank", - "fieldtype": "Read Only", - "label": "Bank" + "fieldtype": "Link", + "label": "Bank", + "options": "Bank", + "read_only": 1 }, { "fetch_from": "bank_account.bank_account_no", @@ -366,10 +368,11 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-09-18 12:24:14.178853", + "modified": "2022-09-30 16:19:43.680025", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Request", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -401,5 +404,6 @@ } ], "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index b126d57400a..eedaaaf338b 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -343,7 +343,8 @@ "no_copy": 1, "options": "POS Invoice", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "default": "0", @@ -1553,7 +1554,7 @@ "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2022-03-22 13:00:24.166684", + "modified": "2022-09-30 03:49:50.455199", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 6e3a0766f10..54a3e934b2d 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -239,14 +239,14 @@ class POSInvoice(SalesInvoice): frappe.bold(d.warehouse), frappe.bold(d.qty), ) - if flt(available_stock) <= 0: + if is_stock_item and flt(available_stock) <= 0: frappe.throw( _("Row #{}: Item Code: {} is not available under warehouse {}.").format( d.idx, item_code, warehouse ), title=_("Item Unavailable"), ) - elif flt(available_stock) < flt(d.qty): + elif is_stock_item and flt(available_stock) < flt(d.qty): frappe.throw( _( "Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}." @@ -632,11 +632,12 @@ def get_stock_availability(item_code, warehouse): pos_sales_qty = get_pos_reserved_qty(item_code, warehouse) return bin_qty - pos_sales_qty, is_stock_item else: - is_stock_item = False + is_stock_item = True if frappe.db.exists("Product Bundle", item_code): return get_bundle_availability(item_code, warehouse), is_stock_item else: - # Is a service item + is_stock_item = False + # Is a service item or non_stock item return 0, is_stock_item @@ -650,7 +651,9 @@ def get_bundle_availability(bundle_item_code, warehouse): available_qty = item_bin_qty - item_pos_reserved_qty max_available_bundles = available_qty / item.qty - if bundle_bin_qty > max_available_bundles: + if bundle_bin_qty > max_available_bundles and frappe.get_value( + "Item", item.item_code, "is_stock_item" + ): bundle_bin_qty = max_available_bundles pos_sales_qty = get_pos_reserved_qty(bundle_item_code, warehouse) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 986fc038c60..3020e6dc6e3 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -98,7 +98,6 @@ "section_break_44", "apply_discount_on", "base_discount_amount", - "additional_discount_account", "column_break_46", "additional_discount_percentage", "discount_amount", @@ -1387,12 +1386,6 @@ "print_hide": 1, "read_only": 1 }, - { - "fieldname": "additional_discount_account", - "fieldtype": "Link", - "label": "Additional Discount Account", - "options": "Account" - }, { "default": "0", "fieldname": "ignore_default_payment_terms_template", @@ -1445,7 +1438,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2022-09-13 23:39:54.525037", + "modified": "2022-09-27 11:07:55.766844", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index d1853002891..2b633cb8c34 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -669,9 +669,6 @@ class PurchaseInvoice(BuyingController): exchange_rate_map, net_rate_map = get_purchase_document_details(self) - enable_discount_accounting = cint( - frappe.db.get_single_value("Buying Settings", "enable_discount_accounting") - ) provisional_accounting_for_non_stock_items = cint( frappe.db.get_value( "Company", self.company, "enable_provisional_accounting_for_non_stock_items" @@ -1159,9 +1156,6 @@ class PurchaseInvoice(BuyingController): def make_tax_gl_entries(self, gl_entries): # tax table gl entries valuation_tax = {} - enable_discount_accounting = cint( - frappe.db.get_single_value("Buying Settings", "enable_discount_accounting") - ) for tax in self.get("taxes"): amount, base_amount = self.get_tax_amounts(tax, None) @@ -1249,15 +1243,6 @@ class PurchaseInvoice(BuyingController): ) ) - @property - def enable_discount_accounting(self): - if not hasattr(self, "_enable_discount_accounting"): - self._enable_discount_accounting = cint( - frappe.db.get_single_value("Buying Settings", "enable_discount_accounting") - ) - - return self._enable_discount_accounting - def make_internal_transfer_gl_entries(self, gl_entries): if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges): account_currency = get_account_currency(self.unrealized_profit_loss_account) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 0a4f25b8769..f901257ccf6 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1543,6 +1543,37 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): pi.save() self.assertEqual(pi.items[0].conversion_factor, 1000) + def test_batch_expiry_for_purchase_invoice(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + item = self.make_item( + "_Test Batch Item For Return Check", + { + "is_purchase_item": 1, + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TBIRC.#####", + }, + ) + + pi = make_purchase_invoice( + qty=1, + item_code=item.name, + update_stock=True, + ) + + pi.load_from_db() + batch_no = pi.items[0].batch_no + self.assertTrue(batch_no) + + frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(nowdate(), -1)) + + return_pi = make_return_doc(pi.doctype, pi.name) + return_pi.save().submit() + + self.assertTrue(return_pi.docstatus == 1) + def check_gl_entries(doc, voucher_no, expected_gle, posting_date): gl_entries = frappe.db.sql( diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 7fa2fe2a668..fca7e3a8873 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -74,7 +74,6 @@ "manufacturer_part_no", "accounting", "expense_account", - "discount_account", "col_break5", "is_fixed_asset", "asset_location", @@ -860,12 +859,6 @@ "print_hide": 1, "read_only": 1 }, - { - "fieldname": "discount_account", - "fieldtype": "Link", - "label": "Discount Account", - "options": "Account" - }, { "fieldname": "product_bundle", "fieldtype": "Link", @@ -877,7 +870,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2022-06-17 05:31:10.520171", + "modified": "2022-09-27 10:54:23.980713", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index e51938b27f5..afd5a59df4e 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -22,9 +22,12 @@ from erpnext.accounts.general_ledger import get_round_off_account_and_cost_cente from erpnext.accounts.party import get_due_date, get_party_account, get_party_details from erpnext.accounts.utils import get_account_currency from erpnext.assets.doctype.asset.depreciation import ( + depreciate_asset, get_disposal_account_and_cost_center, get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_regain, + reset_depreciation_schedule, + reverse_depreciation_entry_made_after_disposal, ) from erpnext.controllers.accounts_controller import validate_account_head from erpnext.controllers.selling_controller import SellingController @@ -1081,23 +1084,25 @@ class SalesInvoice(SellingController): if self.is_return: fixed_asset_gl_entries = get_gl_entries_on_asset_regain( - asset, item.base_net_amount, item.finance_book + asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name") ) asset.db_set("disposal_date", None) if asset.calculate_depreciation: - self.reverse_depreciation_entry_made_after_disposal(asset) - self.reset_depreciation_schedule(asset) + posting_date = frappe.db.get_value("Sales Invoice", self.return_against, "posting_date") + reverse_depreciation_entry_made_after_disposal(asset, posting_date) + reset_depreciation_schedule(asset, self.posting_date) else: + if asset.calculate_depreciation: + depreciate_asset(asset, self.posting_date) + asset.reload() + fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( - asset, item.base_net_amount, item.finance_book + asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name") ) asset.db_set("disposal_date", self.posting_date) - if asset.calculate_depreciation: - self.depreciate_asset(asset) - for gle in fixed_asset_gl_entries: gle["against"] = self.customer gl_entries.append(self.get_gl_dict(gle, item=item)) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 782e08e33ba..ce44ae304b3 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -8,7 +8,7 @@ import frappe from frappe.model.dynamic_links import get_dynamic_link_map from frappe.model.naming import make_autoname from frappe.tests.utils import change_settings -from frappe.utils import add_days, flt, getdate, nowdate +from frappe.utils import add_days, flt, getdate, nowdate, today import erpnext from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account @@ -3196,6 +3196,37 @@ class TestSalesInvoice(unittest.TestCase): "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled ) + def test_batch_expiry_for_sales_invoice_return(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.stock.doctype.item.test_item import make_item + + item = make_item( + "_Test Batch Item For Return Check", + { + "is_purchase_item": 1, + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TBIRC.#####", + }, + ) + + pr = make_purchase_receipt(qty=1, item_code=item.name) + + batch_no = pr.items[0].batch_no + si = create_sales_invoice(qty=1, item_code=item.name, update_stock=1, batch_no=batch_no) + + si.load_from_db() + batch_no = si.items[0].batch_no + self.assertTrue(batch_no) + + frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1)) + + return_si = make_return_doc(si.doctype, si.name) + return_si.save().submit() + + self.assertTrue(return_si.docstatus == 1) + def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() @@ -3289,6 +3320,7 @@ def create_sales_invoice(**args): "serial_no": args.serial_no, "conversion_factor": 1, "incoming_rate": args.incoming_rate or 0, + "batch_no": args.batch_no or None, }, ) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 0b5df9e0cc0..84c2c9a3c3e 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -335,6 +335,9 @@ def get_advance_vouchers( "party": ["in", parties], } + if party_type == "Customer": + filters.update({"against_voucher": ["is", "not set"]}) + if company: filters["company"] = company if from_date and to_date: diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index e77e828e166..82f38dacd2a 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -237,9 +237,9 @@ def get_conditions(filters): or filters.get("party") or filters.get("group_by") in ["Group by Account", "Group by Party"] ): - conditions.append("posting_date >=%(from_date)s") + conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')") - conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')") + conditions.append("(posting_date <=%(to_date)s)") if filters.get("project"): conditions.append("project in %(project)s") diff --git a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py index 9d566785416..cd5f3667071 100644 --- a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py +++ b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py @@ -155,7 +155,6 @@ def adjust_account(data, period_list, consolidated=False): for d in data: for period in period_list: key = period if consolidated else period.key - d[key] = totals[d["account"]] d["total"] = totals[d["account"]] return data diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py index 33bd3c74965..06e3c6120de 100644 --- a/erpnext/accounts/report/sales_register/sales_register.py +++ b/erpnext/accounts/report/sales_register/sales_register.py @@ -370,7 +370,7 @@ def get_conditions(filters): where parent=`tabSales Invoice`.name and ifnull(`tab{table}`.{field}, '') = %({field})s)""" - conditions += get_sales_invoice_item_field_condition("mode_of_payments", "Sales Invoice Payment") + conditions += get_sales_invoice_item_field_condition("mode_of_payment", "Sales Invoice Payment") conditions += get_sales_invoice_item_field_condition("cost_center") conditions += get_sales_invoice_item_field_condition("warehouse") conditions += get_sales_invoice_item_field_condition("brand") diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 6bd08ad837a..6d2cd8ed411 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -172,6 +172,7 @@ def get_rootwise_opening_balances(filters, report_type): query_filters = { "company": filters.company, "from_date": filters.from_date, + "to_date": filters.to_date, "report_type": report_type, "year_start_date": filters.year_start_date, "project": filters.project, @@ -200,7 +201,7 @@ def get_rootwise_opening_balances(filters, report_type): where company=%(company)s {additional_conditions} - and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes') + and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s)) and account in (select name from `tabAccount` where report_type=%(report_type)s) and is_cancelled = 0 group by account""".format( diff --git a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py index 5fcfdff6f1a..ee223484d47 100644 --- a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py +++ b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py @@ -104,12 +104,17 @@ def get_opening_balances(filters): where company=%(company)s and is_cancelled=0 and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != '' - and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes') + and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s)) {account_filter} group by party""".format( account_filter=account_filter ), - {"company": filters.company, "from_date": filters.from_date, "party_type": filters.party_type}, + { + "company": filters.company, + "from_date": filters.from_date, + "to_date": filters.to_date, + "party_type": filters.party_type, + }, as_dict=True, ) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index c5eb7d8733f..9ede67848dd 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -86,7 +86,7 @@ def get_fiscal_years( ) ) - query = query.orderby(FY.year_start_date, Order.desc) + query = query.orderby(FY.year_start_date, order=Order.desc) fiscal_years = query.run(as_dict=True) frappe.cache().hset("fiscal_years", company, fiscal_years) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index a43a16c9ec5..5512d4159d8 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -230,7 +230,7 @@ frappe.ui.form.on('Asset', { datasets: [{ color: 'green', values: asset_values, - formatted: asset_values.map(d => d.toFixed(2)) + formatted: asset_values.map(d => d?.toFixed(2)) }] }, type: 'line' diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 991df4eada6..f0505ff9835 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -388,7 +388,7 @@ "in_standard_filter": 1, "label": "Status", "no_copy": 1, - "options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt", + "options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nDecapitalized", "read_only": 1 }, { diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 8ac7ed6387b..ca6be9b57b2 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -828,7 +828,9 @@ class Asset(AccountsController): def update_maintenance_status(): - assets = frappe.get_all("Asset", filters={"docstatus": 1, "maintenance_required": 1}) + assets = frappe.get_all( + "Asset", filters={"docstatus": 1, "maintenance_required": 1, "disposal_date": ("is", "not set")} + ) for asset in assets: asset = frappe.get_doc("Asset", asset.name) diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 74386384c5d..97941706aa8 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -4,11 +4,12 @@ import frappe from frappe import _ -from frappe.utils import cint, flt, getdate, today +from frappe.utils import add_months, cint, flt, getdate, nowdate, today from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_checks_for_pl_and_bs_accounts, ) +from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry def post_depreciation_entries(date=None, commit=True): @@ -196,6 +197,11 @@ def scrap_asset(asset_name): _("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status) ) + date = today() + + depreciate_asset(asset, date) + asset.reload() + depreciation_series = frappe.get_cached_value( "Company", asset.company, "series_for_depreciation_entry" ) @@ -203,7 +209,7 @@ def scrap_asset(asset_name): je = frappe.new_doc("Journal Entry") je.voucher_type = "Journal Entry" je.naming_series = depreciation_series - je.posting_date = today() + je.posting_date = date je.company = asset.company je.remark = "Scrap Entry for asset {0}".format(asset_name) @@ -214,7 +220,7 @@ def scrap_asset(asset_name): je.flags.ignore_permissions = True je.submit() - frappe.db.set_value("Asset", asset_name, "disposal_date", today()) + frappe.db.set_value("Asset", asset_name, "disposal_date", date) frappe.db.set_value("Asset", asset_name, "journal_entry_for_scrap", je.name) asset.set_status("Scrapped") @@ -225,6 +231,9 @@ def scrap_asset(asset_name): def restore_asset(asset_name): asset = frappe.get_doc("Asset", asset_name) + reverse_depreciation_entry_made_after_disposal(asset, asset.disposal_date) + reset_depreciation_schedule(asset, asset.disposal_date) + je = asset.journal_entry_for_scrap asset.db_set("disposal_date", None) @@ -235,7 +244,94 @@ def restore_asset(asset_name): asset.set_status() -def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None): +def depreciate_asset(asset, date): + asset.flags.ignore_validate_update_after_submit = True + asset.prepare_depreciation_data(date_of_disposal=date) + asset.save() + + make_depreciation_entry(asset.name, date) + + +def reset_depreciation_schedule(asset, date): + asset.flags.ignore_validate_update_after_submit = True + + # recreate original depreciation schedule of the asset + asset.prepare_depreciation_data(date_of_return=date) + + modify_depreciation_schedule_for_asset_repairs(asset) + asset.save() + + +def modify_depreciation_schedule_for_asset_repairs(asset): + asset_repairs = frappe.get_all( + "Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"] + ) + + for repair in asset_repairs: + if repair.increase_in_asset_life: + asset_repair = frappe.get_doc("Asset Repair", repair.name) + asset_repair.modify_depreciation_schedule() + asset.prepare_depreciation_data() + + +def reverse_depreciation_entry_made_after_disposal(asset, date): + row = -1 + finance_book = asset.get("schedules")[0].get("finance_book") + for schedule in asset.get("schedules"): + if schedule.finance_book != finance_book: + row = 0 + finance_book = schedule.finance_book + else: + row += 1 + + if schedule.schedule_date == date: + if not disposal_was_made_on_original_schedule_date( + asset, schedule, row, date + ) or disposal_happens_in_the_future(date): + + reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) + reverse_journal_entry.posting_date = nowdate() + frappe.flags.is_reverse_depr_entry = True + reverse_journal_entry.submit() + + frappe.flags.is_reverse_depr_entry = False + asset.flags.ignore_validate_update_after_submit = True + schedule.journal_entry = None + depreciation_amount = get_depreciation_amount_in_je(reverse_journal_entry) + asset.finance_books[0].value_after_depreciation += depreciation_amount + asset.save() + + +def get_depreciation_amount_in_je(journal_entry): + if journal_entry.accounts[0].debit_in_account_currency: + return journal_entry.accounts[0].debit_in_account_currency + else: + return journal_entry.accounts[0].credit_in_account_currency + + +# if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone +def disposal_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_disposal): + for finance_book in asset.get("finance_books"): + if schedule.finance_book == finance_book.finance_book: + orginal_schedule_date = add_months( + finance_book.depreciation_start_date, row * cint(finance_book.frequency_of_depreciation) + ) + + if orginal_schedule_date == posting_date_of_disposal: + return True + return False + + +def disposal_happens_in_the_future(posting_date_of_disposal): + if posting_date_of_disposal > getdate(): + return True + + return False + + +def get_gl_entries_on_asset_regain( + asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None +): ( fixed_asset_account, asset, @@ -247,28 +343,45 @@ def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None): ) = get_asset_details(asset, finance_book) gl_entries = [ - { - "account": fixed_asset_account, - "debit_in_account_currency": asset.gross_purchase_amount, - "debit": asset.gross_purchase_amount, - "cost_center": depreciation_cost_center, - }, - { - "account": accumulated_depr_account, - "credit_in_account_currency": accumulated_depr_amount, - "credit": accumulated_depr_amount, - "cost_center": depreciation_cost_center, - }, + asset.get_gl_dict( + { + "account": fixed_asset_account, + "debit_in_account_currency": asset.gross_purchase_amount, + "debit": asset.gross_purchase_amount, + "cost_center": depreciation_cost_center, + "posting_date": getdate(), + }, + item=asset, + ), + asset.get_gl_dict( + { + "account": accumulated_depr_account, + "credit_in_account_currency": accumulated_depr_amount, + "credit": accumulated_depr_amount, + "cost_center": depreciation_cost_center, + "posting_date": getdate(), + }, + item=asset, + ), ] profit_amount = abs(flt(value_after_depreciation)) - abs(flt(selling_amount)) if profit_amount: - get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center) + get_profit_gl_entries( + asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center + ) + + if voucher_type and voucher_no: + for entry in gl_entries: + entry["voucher_type"] = voucher_type + entry["voucher_no"] = voucher_no return gl_entries -def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None): +def get_gl_entries_on_asset_disposal( + asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None +): ( fixed_asset_account, asset, @@ -280,23 +393,38 @@ def get_gl_entries_on_asset_disposal(asset, selling_amount=0, finance_book=None) ) = get_asset_details(asset, finance_book) gl_entries = [ - { - "account": fixed_asset_account, - "credit_in_account_currency": asset.gross_purchase_amount, - "credit": asset.gross_purchase_amount, - "cost_center": depreciation_cost_center, - }, - { - "account": accumulated_depr_account, - "debit_in_account_currency": accumulated_depr_amount, - "debit": accumulated_depr_amount, - "cost_center": depreciation_cost_center, - }, + asset.get_gl_dict( + { + "account": fixed_asset_account, + "credit_in_account_currency": asset.gross_purchase_amount, + "credit": asset.gross_purchase_amount, + "cost_center": depreciation_cost_center, + "posting_date": getdate(), + }, + item=asset, + ), + asset.get_gl_dict( + { + "account": accumulated_depr_account, + "debit_in_account_currency": accumulated_depr_amount, + "debit": accumulated_depr_amount, + "cost_center": depreciation_cost_center, + "posting_date": getdate(), + }, + item=asset, + ), ] profit_amount = flt(selling_amount) - flt(value_after_depreciation) if profit_amount: - get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center) + get_profit_gl_entries( + asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center + ) + + if voucher_type and voucher_no: + for entry in gl_entries: + entry["voucher_type"] = voucher_type + entry["voucher_no"] = voucher_no return gl_entries @@ -333,15 +461,21 @@ def get_asset_details(asset, finance_book=None): ) -def get_profit_gl_entries(profit_amount, gl_entries, disposal_account, depreciation_cost_center): +def get_profit_gl_entries( + asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center +): debit_or_credit = "debit" if profit_amount < 0 else "credit" gl_entries.append( - { - "account": disposal_account, - "cost_center": depreciation_cost_center, - debit_or_credit: abs(profit_amount), - debit_or_credit + "_in_account_currency": abs(profit_amount), - } + asset.get_gl_dict( + { + "account": disposal_account, + "cost_center": depreciation_cost_center, + debit_or_credit: abs(profit_amount), + debit_or_credit + "_in_account_currency": abs(profit_amount), + "posting_date": getdate(), + }, + item=asset, + ) ) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index e7af9bd5bc2..370b13bb98d 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -4,10 +4,23 @@ import unittest import frappe -from frappe.utils import add_days, add_months, cstr, flt, get_last_day, getdate, nowdate +from frappe.utils import ( + add_days, + add_months, + cstr, + flt, + get_first_day, + get_last_day, + getdate, + nowdate, +) from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice -from erpnext.assets.doctype.asset.asset import make_sales_invoice, split_asset +from erpnext.assets.doctype.asset.asset import ( + make_sales_invoice, + split_asset, + update_maintenance_status, +) from erpnext.assets.doctype.asset.depreciation import ( post_depreciation_entries, restore_asset, @@ -178,28 +191,48 @@ class TestAsset(AssetSetup): self.assertEqual(doc.items[0].is_fixed_asset, 1) def test_scrap_asset(self): + date = nowdate() + purchase_date = add_months(get_first_day(date), -2) + asset = create_asset( calculate_depreciation=1, - available_for_use_date="2020-01-01", - purchase_date="2020-01-01", + available_for_use_date=purchase_date, + purchase_date=purchase_date, expected_value_after_useful_life=10000, total_number_of_depreciations=10, frequency_of_depreciation=1, submit=1, ) - post_depreciation_entries(date=add_months("2020-01-01", 4)) + post_depreciation_entries(date=add_months(purchase_date, 2)) + asset.load_from_db() + + accumulated_depr_amount = flt( + asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation, + asset.precision("gross_purchase_amount"), + ) + self.assertEquals(accumulated_depr_amount, 18000.0) scrap_asset(asset.name) - asset.load_from_db() + + accumulated_depr_amount = flt( + asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation, + asset.precision("gross_purchase_amount"), + ) + pro_rata_amount, _, _ = asset.get_pro_rata_amt( + asset.finance_books[0], 9000, add_months(get_last_day(purchase_date), 1), date + ) + pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount")) + self.assertEquals(accumulated_depr_amount, 18000.00 + pro_rata_amount) + self.assertEqual(asset.status, "Scrapped") self.assertTrue(asset.journal_entry_for_scrap) expected_gle = ( - ("_Test Accumulated Depreciations - _TC", 36000.0, 0.0), + ("_Test Accumulated Depreciations - _TC", 18000.0 + pro_rata_amount, 0.0), ("_Test Fixed Asset - _TC", 0.0, 100000.0), - ("_Test Gain/Loss on Asset Disposal - _TC", 64000.0, 0.0), + ("_Test Gain/Loss on Asset Disposal - _TC", 82000.0 - pro_rata_amount, 0.0), ) gle = frappe.db.sql( @@ -216,7 +249,64 @@ class TestAsset(AssetSetup): self.assertFalse(asset.journal_entry_for_scrap) self.assertEqual(asset.status, "Partially Depreciated") + accumulated_depr_amount = flt( + asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation, + asset.precision("gross_purchase_amount"), + ) + this_month_depr_amount = 9000.0 if get_last_day(date) == date else 0 + + self.assertEquals(accumulated_depr_amount, 18000.0 + this_month_depr_amount) + def test_gle_made_by_asset_sale(self): + date = nowdate() + purchase_date = add_months(get_first_day(date), -2) + + asset = create_asset( + calculate_depreciation=1, + available_for_use_date=purchase_date, + purchase_date=purchase_date, + expected_value_after_useful_life=10000, + total_number_of_depreciations=10, + frequency_of_depreciation=1, + submit=1, + ) + + post_depreciation_entries(date=add_months(purchase_date, 2)) + + si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company") + si.customer = "_Test Customer" + si.due_date = nowdate() + si.get("items")[0].rate = 25000 + si.insert() + si.submit() + + self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") + + pro_rata_amount, _, _ = asset.get_pro_rata_amt( + asset.finance_books[0], 9000, add_months(get_last_day(purchase_date), 1), date + ) + pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount")) + + expected_gle = ( + ("_Test Accumulated Depreciations - _TC", 18000.0 + pro_rata_amount, 0.0), + ("_Test Fixed Asset - _TC", 0.0, 100000.0), + ("_Test Gain/Loss on Asset Disposal - _TC", 57000.0 - pro_rata_amount, 0.0), + ("Debtors - _TC", 25000.0, 0.0), + ) + + gle = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` + where voucher_type='Sales Invoice' and voucher_no = %s + order by account""", + si.name, + ) + + self.assertSequenceEqual(gle, expected_gle) + + si.cancel() + self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated") + + def test_asset_with_maintenance_required_status_after_sale(self): asset = create_asset( calculate_depreciation=1, available_for_use_date="2020-06-06", @@ -224,6 +314,7 @@ class TestAsset(AssetSetup): expected_value_after_useful_life=10000, total_number_of_depreciations=3, frequency_of_depreciation=10, + maintenance_required=1, depreciation_start_date="2020-12-31", submit=1, ) @@ -239,24 +330,9 @@ class TestAsset(AssetSetup): self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") - expected_gle = ( - ("_Test Accumulated Depreciations - _TC", 20490.2, 0.0), - ("_Test Fixed Asset - _TC", 0.0, 100000.0), - ("_Test Gain/Loss on Asset Disposal - _TC", 54509.8, 0.0), - ("Debtors - _TC", 25000.0, 0.0), - ) + update_maintenance_status() - gle = frappe.db.sql( - """select account, debit, credit from `tabGL Entry` - where voucher_type='Sales Invoice' and voucher_no = %s - order by account""", - si.name, - ) - - self.assertSequenceEqual(gle, expected_gle) - - si.cancel() - self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated") + self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") def test_asset_splitting(self): asset = create_asset( @@ -1376,6 +1452,7 @@ def create_asset(**args): "number_of_depreciations_booked": args.number_of_depreciations_booked or 0, "gross_purchase_amount": args.gross_purchase_amount or 100000, "purchase_receipt_amount": args.purchase_receipt_amount or 100000, + "maintenance_required": args.maintenance_required or 0, "warehouse": args.warehouse or "_Test Warehouse - _TC", "available_for_use_date": args.available_for_use_date or "2020-06-06", "location": args.location or "Test Location", diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 2e6f0ad7b02..08355f047e5 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -12,8 +12,11 @@ from six import string_types import erpnext from erpnext.assets.doctype.asset.depreciation import ( + depreciate_asset, get_gl_entries_on_asset_disposal, get_value_after_depreciation_on_disposal_date, + reset_depreciation_schedule, + reverse_depreciation_entry_made_after_disposal, ) from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import ( @@ -424,11 +427,15 @@ class AssetCapitalization(StockController): asset = self.get_asset(item) if asset.calculate_depreciation: - self.depreciate_asset(asset) + depreciate_asset(asset, self.posting_date) asset.reload() fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( - asset, item.asset_value, item.get("finance_book") or self.get("finance_book") + asset, + item.asset_value, + item.get("finance_book") or self.get("finance_book"), + self.get("doctype"), + self.get("name"), ) asset.db_set("disposal_date", self.posting_date) @@ -516,8 +523,8 @@ class AssetCapitalization(StockController): self.set_consumed_asset_status(asset) if asset.calculate_depreciation: - self.reverse_depreciation_entry_made_after_disposal(asset) - self.reset_depreciation_schedule(asset) + reverse_depreciation_entry_made_after_disposal(asset, self.posting_date) + reset_depreciation_schedule(asset, self.posting_date) def get_asset(self, item): asset = frappe.get_doc("Asset", item.asset) diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index aad26075f2d..28158a31b94 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -20,7 +20,6 @@ "maintain_same_rate", "allow_multiple_items", "bill_for_rejected_quantity_in_purchase_invoice", - "enable_discount_accounting", "subcontract", "backflush_raw_materials_of_subcontract_based_on", "column_break_11", @@ -134,13 +133,6 @@ { "fieldname": "column_break_12", "fieldtype": "Column Break" - }, - { - "default": "0", - "description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account", - "fieldname": "enable_discount_accounting", - "fieldtype": "Check", - "label": "Enable Discount Accounting for Buying" } ], "icon": "fa fa-cog", @@ -148,7 +140,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-09-01 18:01:34.994657", + "modified": "2022-09-27 10:50:27.050252", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.py b/erpnext/buying/doctype/buying_settings/buying_settings.py index 7b18cdbedcd..be1ebdeb64e 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.py +++ b/erpnext/buying/doctype/buying_settings/buying_settings.py @@ -5,15 +5,10 @@ import frappe -from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.model.document import Document -from frappe.utils import cint class BuyingSettings(Document): - def on_update(self): - self.toggle_discount_accounting_fields() - def validate(self): for key in ["supplier_group", "supp_master_name", "maintain_same_rate", "buying_price_list"]: frappe.db.set_default(key, self.get(key, "")) @@ -26,60 +21,3 @@ class BuyingSettings(Document): self.get("supp_master_name") == "Naming Series", hide_name_field=False, ) - - def toggle_discount_accounting_fields(self): - enable_discount_accounting = cint(self.enable_discount_accounting) - - make_property_setter( - "Purchase Invoice Item", - "discount_account", - "hidden", - not (enable_discount_accounting), - "Check", - validate_fields_for_doctype=False, - ) - if enable_discount_accounting: - make_property_setter( - "Purchase Invoice Item", - "discount_account", - "mandatory_depends_on", - "eval: doc.discount_amount", - "Code", - validate_fields_for_doctype=False, - ) - else: - make_property_setter( - "Purchase Invoice Item", - "discount_account", - "mandatory_depends_on", - "", - "Code", - validate_fields_for_doctype=False, - ) - - make_property_setter( - "Purchase Invoice", - "additional_discount_account", - "hidden", - not (enable_discount_accounting), - "Check", - validate_fields_for_doctype=False, - ) - if enable_discount_accounting: - make_property_setter( - "Purchase Invoice", - "additional_discount_account", - "mandatory_depends_on", - "eval: doc.discount_amount", - "Code", - validate_fields_for_doctype=False, - ) - else: - make_property_setter( - "Purchase Invoice", - "additional_discount_account", - "mandatory_depends_on", - "", - "Code", - validate_fields_for_doctype=False, - ) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index fc99d776d4a..ddf81ca3aec 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -33,6 +33,7 @@ frappe.ui.form.on("Purchase Order", { frm.set_query("fg_item", "items", function() { return { filters: { + 'is_stock_item': 1, 'is_sub_contracted_item': 1, 'default_bom': ['!=', ''] } diff --git a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py index dbdc62e9ec7..d089473a16a 100644 --- a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py +++ b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.py @@ -53,4 +53,5 @@ def get_chart_data(data, conditions, filters): }, "type": "line", "lineOptions": {"regionFill": 1}, + "fieldtype": "Currency", } diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 8686cb5cc09..22291a35441 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -38,7 +38,6 @@ from erpnext.accounts.party import ( validate_party_frozen_disabled, ) from erpnext.accounts.utils import get_account_currency, get_fiscal_years, validate_fiscal_year -from erpnext.assets.doctype.asset.depreciation import make_depreciation_entry from erpnext.buying.utils import update_last_purchase_rate from erpnext.controllers.print_settings import ( set_print_templates_for_item_table, @@ -1891,88 +1890,6 @@ class AccountsController(TransactionBase): _("Select finance book for the item {0} at row {1}").format(item.item_code, item.idx) ) - def depreciate_asset(self, asset): - asset.flags.ignore_validate_update_after_submit = True - asset.prepare_depreciation_data(date_of_disposal=self.posting_date) - asset.save() - - make_depreciation_entry(asset.name, self.posting_date) - - def reset_depreciation_schedule(self, asset): - asset.flags.ignore_validate_update_after_submit = True - - # recreate original depreciation schedule of the asset - asset.prepare_depreciation_data(date_of_return=self.posting_date) - - self.modify_depreciation_schedule_for_asset_repairs(asset) - asset.save() - - def modify_depreciation_schedule_for_asset_repairs(self, asset): - asset_repairs = frappe.get_all( - "Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"] - ) - - for repair in asset_repairs: - if repair.increase_in_asset_life: - asset_repair = frappe.get_doc("Asset Repair", repair.name) - asset_repair.modify_depreciation_schedule() - asset.prepare_depreciation_data() - - def reverse_depreciation_entry_made_after_disposal(self, asset): - from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry - - posting_date_of_original_disposal = self.get_posting_date_of_disposal_entry() - - row = -1 - finance_book = asset.get("schedules")[0].get("finance_book") - for schedule in asset.get("schedules"): - if schedule.finance_book != finance_book: - row = 0 - finance_book = schedule.finance_book - else: - row += 1 - - if schedule.schedule_date == posting_date_of_original_disposal: - if not self.disposal_was_made_on_original_schedule_date( - asset, schedule, row, posting_date_of_original_disposal - ) or self.disposal_happens_in_the_future(posting_date_of_original_disposal): - - reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) - reverse_journal_entry.posting_date = nowdate() - frappe.flags.is_reverse_depr_entry = True - reverse_journal_entry.submit() - - frappe.flags.is_reverse_depr_entry = False - asset.flags.ignore_validate_update_after_submit = True - schedule.journal_entry = None - asset.save() - - def get_posting_date_of_disposal_entry(self): - if self.doctype == "Sales Invoice" and self.return_against: - return frappe.db.get_value("Sales Invoice", self.return_against, "posting_date") - else: - return self.posting_date - - # if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone - def disposal_was_made_on_original_schedule_date( - self, asset, schedule, row, posting_date_of_disposal - ): - for finance_book in asset.get("finance_books"): - if schedule.finance_book == finance_book.finance_book: - orginal_schedule_date = add_months( - finance_book.depreciation_start_date, row * cint(finance_book.frequency_of_depreciation) - ) - - if orginal_schedule_date == posting_date_of_disposal: - return True - return False - - def disposal_happens_in_the_future(self, posting_date_of_disposal): - if posting_date_of_disposal > getdate(): - return True - - return False - @frappe.whitelist() def get_tax_rate(account_head): diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 4f8b5c79d24..8eae0a07028 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -212,21 +212,15 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals meta = frappe.get_meta(doctype, cached=True) searchfields = meta.get_search_fields() - # these are handled separately - ignored_search_fields = ("item_name", "description") - for ignored_field in ignored_search_fields: - if ignored_field in searchfields: - searchfields.remove(ignored_field) - columns = "" - extra_searchfields = [ - field - for field in searchfields - if not field in ["name", "item_group", "description", "item_name"] - ] + extra_searchfields = [field for field in searchfields if not field in ["name", "description"]] if extra_searchfields: - columns = ", " + ", ".join(extra_searchfields) + columns += ", " + ", ".join(extra_searchfields) + + if "description" in searchfields: + columns += """, if(length(tabItem.description) > 40, \ + concat(substr(tabItem.description, 1, 40), "..."), description) as description""" searchfields = searchfields + [ field @@ -266,12 +260,10 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals if frappe.db.count(doctype, cache=True) < 50000: # scan description only if items are less than 50000 description_cond = "or tabItem.description LIKE %(txt)s" + return frappe.db.sql( """select - tabItem.name, tabItem.item_name, tabItem.item_group, - if(length(tabItem.description) > 40, \ - concat(substr(tabItem.description, 1, 40), "..."), description) as description - {columns} + tabItem.name {columns} from tabItem where tabItem.docstatus < 2 and tabItem.disabled=0 diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 202a880750e..aa4468c04e4 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -69,9 +69,18 @@ class SubcontractingController(StockController): def validate_items(self): for item in self.items: - if not frappe.get_value("Item", item.item_code, "is_sub_contracted_item"): + is_stock_item, is_sub_contracted_item = frappe.get_value( + "Item", item.item_code, ["is_stock_item", "is_sub_contracted_item"] + ) + + if not is_stock_item: + msg = f"Item {item.item_name} must be a stock item." + frappe.throw(_(msg)) + + if not is_sub_contracted_item: msg = f"Item {item.item_name} must be a subcontracted item." frappe.throw(_(msg)) + if item.bom: bom = frappe.get_doc("BOM", item.bom) if not bom.is_active: @@ -841,7 +850,7 @@ def make_rm_stock_entry( for fg_item_code in fg_item_code_list: for rm_item in rm_items: - if rm_item.get("main_item_code") or rm_item.get("item_code") == fg_item_code: + if rm_item.get("main_item_code") == fg_item_code or rm_item.get("item_code") == fg_item_code: rm_item_code = rm_item.get("rm_item_code") items_dict = { diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js index 116db2f5a27..7cd1710a7f2 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js @@ -44,7 +44,7 @@ frappe.query_reports["Opportunity Summary by Sales Stage"] = { }, { fieldname: "opportunity_source", - label: __("Oppoturnity Source"), + label: __("Opportunity Source"), fieldtype: "Link", options: "Lead Source", }, @@ -62,4 +62,4 @@ frappe.query_reports["Opportunity Summary by Sales Stage"] = { default: frappe.defaults.get_user_default("Company") } ] -}; \ No newline at end of file +}; diff --git a/erpnext/e_commerce/doctype/website_item/website_item.json b/erpnext/e_commerce/doctype/website_item/website_item.json index c5775ee9075..6556eabf4ab 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.json +++ b/erpnext/e_commerce/doctype/website_item/website_item.json @@ -188,7 +188,8 @@ "in_list_view": 1, "label": "Item Group", "options": "Item Group", - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "default": "1", @@ -234,7 +235,8 @@ "fieldname": "brand", "fieldtype": "Link", "label": "Brand", - "options": "Brand" + "options": "Brand", + "search_index": 1 }, { "collapsible": 1, @@ -346,7 +348,7 @@ "index_web_pages_for_search": 1, "links": [], "make_attachments_public": 1, - "modified": "2022-09-13 04:05:11.614087", + "modified": "2022-09-30 04:01:52.090732", "modified_by": "Administrator", "module": "E-commerce", "name": "Website Item", diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py index c0f8c79283d..3e5d5f768fa 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.py +++ b/erpnext/e_commerce/doctype/website_item/website_item.py @@ -403,9 +403,6 @@ def on_doctype_update(): # since route is a Text column, it needs a length for indexing frappe.db.add_index("Website Item", ["route(500)"]) - frappe.db.add_index("Website Item", ["item_group"]) - frappe.db.add_index("Website Item", ["brand"]) - def check_if_user_is_customer(user=None): from frappe.contacts.doctype.contact.contact import get_contact_name diff --git a/erpnext/hooks.py b/erpnext/hooks.py index a08feb44476..b8f51f839ce 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -391,12 +391,12 @@ scheduler_events = { "erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts", ], "hourly": [ - "erpnext.accounts.doctype.subscription.subscription.process_all", "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization", "erpnext.projects.doctype.project.project.hourly_reminder", "erpnext.projects.doctype.project.project.collect_project_status", ], "hourly_long": [ + "erpnext.accounts.doctype.subscription.subscription.process_all", "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries", "erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction", ], @@ -508,6 +508,7 @@ accounting_dimension_doctypes = [ "Landed Cost Item", "Asset Value Adjustment", "Asset Repair", + "Asset Capitalization", "Loyalty Program", "Stock Reconciliation", "POS Profile", diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 70637d3ef20..ff84991c36e 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1019,7 +1019,6 @@ def get_bom_items_as_dict( where bom_item.docstatus < 2 and bom.name = %(bom)s - and ifnull(item.has_variants, 0) = 0 and item.is_stock_item in (1, {is_stock_item}) {where_conditions} group by item_code, stock_uom diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 20f15039efe..f3640b93b22 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -557,37 +557,52 @@ erpnext.work_order = { if(!frm.doc.skip_transfer){ // If "Material Consumption is check in Manufacturing Settings, allow Material Consumption - if ((flt(doc.produced_qty) < flt(doc.material_transferred_for_manufacturing)) - && frm.doc.status != 'Stopped') { - frm.has_finish_btn = true; + if (flt(doc.material_transferred_for_manufacturing) > 0 && frm.doc.status != 'Stopped') { + if ((flt(doc.produced_qty) < flt(doc.material_transferred_for_manufacturing))) { + frm.has_finish_btn = true; - if (frm.doc.__onload && frm.doc.__onload.material_consumption == 1) { - // Only show "Material Consumption" when required_qty > consumed_qty - var counter = 0; - var tbl = frm.doc.required_items || []; - var tbl_lenght = tbl.length; - for (var i = 0, len = tbl_lenght; i < len; i++) { - let wo_item_qty = frm.doc.required_items[i].transferred_qty || frm.doc.required_items[i].required_qty; - if (flt(wo_item_qty) > flt(frm.doc.required_items[i].consumed_qty)) { - counter += 1; + if (frm.doc.__onload && frm.doc.__onload.material_consumption == 1) { + // Only show "Material Consumption" when required_qty > consumed_qty + var counter = 0; + var tbl = frm.doc.required_items || []; + var tbl_lenght = tbl.length; + for (var i = 0, len = tbl_lenght; i < len; i++) { + let wo_item_qty = frm.doc.required_items[i].transferred_qty || frm.doc.required_items[i].required_qty; + if (flt(wo_item_qty) > flt(frm.doc.required_items[i].consumed_qty)) { + counter += 1; + } + } + if (counter > 0) { + var consumption_btn = frm.add_custom_button(__('Material Consumption'), function() { + const backflush_raw_materials_based_on = frm.doc.__onload.backflush_raw_materials_based_on; + erpnext.work_order.make_consumption_se(frm, backflush_raw_materials_based_on); + }); + consumption_btn.addClass('btn-primary'); } } - if (counter > 0) { - var consumption_btn = frm.add_custom_button(__('Material Consumption'), function() { - const backflush_raw_materials_based_on = frm.doc.__onload.backflush_raw_materials_based_on; - erpnext.work_order.make_consumption_se(frm, backflush_raw_materials_based_on); - }); - consumption_btn.addClass('btn-primary'); + + var finish_btn = frm.add_custom_button(__('Finish'), function() { + erpnext.work_order.make_se(frm, 'Manufacture'); + }); + + if(doc.material_transferred_for_manufacturing>=doc.qty) { + // all materials transferred for manufacturing, make this primary + finish_btn.addClass('btn-primary'); } - } + } else { + frappe.db.get_doc("Manufacturing Settings").then((doc) => { + let allowance_percentage = doc.overproduction_percentage_for_work_order; - var finish_btn = frm.add_custom_button(__('Finish'), function() { - erpnext.work_order.make_se(frm, 'Manufacture'); - }); + if (allowance_percentage > 0) { + let allowed_qty = frm.doc.qty + ((allowance_percentage / 100) * frm.doc.qty); - if(doc.material_transferred_for_manufacturing>=doc.qty) { - // all materials transferred for manufacturing, make this primary - finish_btn.addClass('btn-primary'); + if ((flt(doc.produced_qty) < allowed_qty)) { + frm.add_custom_button(__('Finish'), function() { + erpnext.work_order.make_se(frm, 'Manufacture'); + }); + } + } + }); } } } else { diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.py b/erpnext/manufacturing/report/job_card_summary/job_card_summary.py index 5083b7369de..63c2d97d574 100644 --- a/erpnext/manufacturing/report/job_card_summary/job_card_summary.py +++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.py @@ -85,8 +85,8 @@ def get_chart_data(job_card_details, filters): open_job_cards.append(periodic_data.get("Open").get(d)) completed.append(periodic_data.get("Completed").get(d)) - datasets.append({"name": "Open", "values": open_job_cards}) - datasets.append({"name": "Completed", "values": completed}) + datasets.append({"name": _("Open"), "values": open_job_cards}) + datasets.append({"name": _("Completed"), "values": completed}) chart = {"data": {"labels": labels, "datasets": datasets}, "type": "bar"} diff --git a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py index 2368bfdf2c6..41ffcbb1904 100644 --- a/erpnext/manufacturing/report/work_order_summary/work_order_summary.py +++ b/erpnext/manufacturing/report/work_order_summary/work_order_summary.py @@ -83,6 +83,7 @@ def get_chart_based_on_status(data): for d in data: status_wise_data[d.status] += 1 + labels = [_(label) for label in labels] values = [status_wise_data[label] for label in labels] chart = { @@ -95,7 +96,7 @@ def get_chart_based_on_status(data): def get_chart_based_on_age(data): - labels = ["0-30 Days", "30-60 Days", "60-90 Days", "90 Above"] + labels = [_("0-30 Days"), _("30-60 Days"), _("60-90 Days"), _("90 Above")] age_wise_data = {"0-30 Days": 0, "30-60 Days": 0, "60-90 Days": 0, "90 Above": 0} @@ -135,8 +136,8 @@ def get_chart_based_on_qty(data, filters): pending.append(periodic_data.get("Pending").get(d)) completed.append(periodic_data.get("Completed").get(d)) - datasets.append({"name": "Pending", "values": pending}) - datasets.append({"name": "Completed", "values": completed}) + datasets.append({"name": _("Pending"), "values": pending}) + datasets.append({"name": _("Completed"), "values": completed}) chart = { "data": {"labels": labels, "datasets": datasets}, diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 2a0ca8c4961..fc63f124e15 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -315,3 +315,4 @@ erpnext.patches.v14_0.fix_crm_no_of_employees erpnext.patches.v14_0.create_accounting_dimensions_in_subcontracting_doctypes erpnext.patches.v14_0.fix_subcontracting_receipt_gl_entries erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger +erpnext.patches.v14_0.create_accounting_dimensions_for_asset_capitalization diff --git a/erpnext/patches/v13_0/update_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py index a1d40b739eb..0bd3fcdec4c 100644 --- a/erpnext/patches/v13_0/update_old_loans.py +++ b/erpnext/patches/v13_0/update_old_loans.py @@ -100,6 +100,7 @@ def execute(): "mode_of_payment": loan.mode_of_payment, "loan_account": loan.loan_account, "payment_account": loan.payment_account, + "disbursement_account": loan.payment_account, "interest_income_account": loan.interest_income_account, "penalty_income_account": loan.penalty_income_account, }, @@ -190,6 +191,7 @@ def create_loan_type(loan, loan_type_name, penalty_account): loan_type_doc.company = loan.company loan_type_doc.mode_of_payment = loan.mode_of_payment loan_type_doc.payment_account = loan.payment_account + loan_type_doc.disbursement_account = loan.payment_account loan_type_doc.loan_account = loan.loan_account loan_type_doc.interest_income_account = loan.interest_income_account loan_type_doc.penalty_income_account = penalty_account diff --git a/erpnext/patches/v14_0/create_accounting_dimensions_for_asset_capitalization.py b/erpnext/patches/v14_0/create_accounting_dimensions_for_asset_capitalization.py new file mode 100644 index 00000000000..09e20a9d794 --- /dev/null +++ b/erpnext/patches/v14_0/create_accounting_dimensions_for_asset_capitalization.py @@ -0,0 +1,31 @@ +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_field + + +def execute(): + accounting_dimensions = frappe.db.get_all( + "Accounting Dimension", fields=["fieldname", "label", "document_type", "disabled"] + ) + + if not accounting_dimensions: + return + + doctype = "Asset Capitalization" + + for d in accounting_dimensions: + field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname}) + + if field: + continue + + df = { + "fieldname": d.fieldname, + "label": d.label, + "fieldtype": "Link", + "options": d.document_type, + "insert_after": "accounting_dimensions_section", + } + + create_custom_field(doctype, df, ignore_validate=True) + + frappe.clear_cache(doctype=doctype) diff --git a/erpnext/patches/v14_0/migrate_remarks_from_gl_to_payment_ledger.py b/erpnext/patches/v14_0/migrate_remarks_from_gl_to_payment_ledger.py index 062d24b78bd..fd2a2a39cc6 100644 --- a/erpnext/patches/v14_0/migrate_remarks_from_gl_to_payment_ledger.py +++ b/erpnext/patches/v14_0/migrate_remarks_from_gl_to_payment_ledger.py @@ -3,6 +3,29 @@ from frappe import qb from frappe.utils import create_batch +def remove_duplicate_entries(pl_entries): + unique_vouchers = set() + for x in pl_entries: + unique_vouchers.add( + (x.company, x.account, x.party_type, x.party, x.voucher_type, x.voucher_no, x.gle_remarks) + ) + + entries = [] + for x in unique_vouchers: + entries.append( + frappe._dict( + company=x[0], + account=x[1], + party_type=x[2], + party=x[3], + voucher_type=x[4], + voucher_no=x[5], + gle_remarks=x[6], + ) + ) + return entries + + def execute(): if frappe.reload_doc("accounts", "doctype", "payment_ledger_entry"): @@ -34,6 +57,8 @@ def execute(): .run(as_dict=True) ) + pl_entries = remove_duplicate_entries(pl_entries) + if pl_entries: # split into multiple batches, update and commit for each batch batch_size = 1000 diff --git a/erpnext/projects/report/project_summary/project_summary.py b/erpnext/projects/report/project_summary/project_summary.py index 606c0c2d81d..7a35fd236a0 100644 --- a/erpnext/projects/report/project_summary/project_summary.py +++ b/erpnext/projects/report/project_summary/project_summary.py @@ -91,9 +91,9 @@ def get_chart_data(data): "data": { "labels": labels[:30], "datasets": [ - {"name": "Overdue", "values": overdue[:30]}, - {"name": "Completed", "values": completed[:30]}, - {"name": "Total Tasks", "values": total[:30]}, + {"name": _("Overdue"), "values": overdue[:30]}, + {"name": _("Completed"), "values": completed[:30]}, + {"name": _("Total Tasks"), "values": total[:30]}, ], }, "type": "bar", diff --git a/erpnext/public/js/help_links.js b/erpnext/public/js/help_links.js index b643ccae947..1c3f43e9cf4 100644 --- a/erpnext/public/js/help_links.js +++ b/erpnext/public/js/help_links.js @@ -671,7 +671,7 @@ frappe.help.help_links["List/Item"] = [ label: "Item Valuation", url: docsUrl + - "user/manual/en/stock/articles/item-valuation-fifo-and-moving-average", + "user/manual/en/stock/articles/calculation-of-valuation-rate-in-fifo-and-moving-average", }, ]; diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js index a6bff2c148d..83b108b8746 100644 --- a/erpnext/public/js/utils/barcode_scanner.js +++ b/erpnext/public/js/utils/barcode_scanner.js @@ -21,6 +21,11 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { this.items_table_name = opts.items_table_name || "items"; this.items_table = this.frm.doc[this.items_table_name]; + // optional sound name to play when scan either fails or passes. + // see https://frappeframework.com/docs/v14/user/en/python-api/hooks#sounds + this.success_sound = opts.play_success_sound; + this.fail_sound = opts.play_fail_sound; + // any API that takes `search_value` as input and returns dictionary as follows // { // item_code: "HORSESHOE", // present if any item was found @@ -54,19 +59,24 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { if (!data || Object.keys(data).length === 0) { this.show_alert(__("Cannot find Item with this Barcode"), "red"); this.clean_up(); + this.play_fail_sound(); reject(); return; } me.update_table(data).then(row => { - row ? resolve(row) : reject(); + this.play_success_sound(); + resolve(row); + }).catch(() => { + this.play_fail_sound(); + reject(); }); }); }); } update_table(data) { - return new Promise(resolve => { + return new Promise((resolve, reject) => { let cur_grid = this.frm.fields_dict[this.items_table_name].grid; const {item_code, barcode, batch_no, serial_no, uom} = data; @@ -77,6 +87,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { if (this.dont_allow_new_row) { this.show_alert(__("Maximum quantity scanned for item {0}.", [item_code]), "red"); this.clean_up(); + reject(); return; } @@ -88,6 +99,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { if (this.is_duplicate_serial_no(row, serial_no)) { this.clean_up(); + reject(); return; } @@ -219,6 +231,14 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { return this.items_table.find((d) => !d.item_code); } + play_success_sound() { + this.success_sound && frappe.utils.play_sound(this.success_sound); + } + + play_fail_sound() { + this.fail_sound && frappe.utils.play_sound(this.fail_sound); + } + clean_up() { this.scan_barcode_field.set_value(""); refresh_field(this.items_table_name); diff --git a/erpnext/public/scss/order-page.scss b/erpnext/public/scss/order-page.scss new file mode 100644 index 00000000000..6f5fe5d4d7a --- /dev/null +++ b/erpnext/public/scss/order-page.scss @@ -0,0 +1,115 @@ +#page-order { + .main-column { + .page-content-wrapper { + + .breadcrumb-container { + @media screen and (min-width: 567px) { + padding-left: var(--padding-sm); + } + } + + .container.my-4 { + background-color: var(--fg-color); + + @media screen and (min-width: 567px) { + padding: 1.25rem 1.5rem; + border-radius: var(--border-radius-md); + box-shadow: var(--card-shadow); + } + } + } + } +} + +.indicator-container { + @media screen and (max-width: 567px) { + padding-bottom: 0.8rem; + } +} + +.order-items { + padding: 1.5rem 0; + border-bottom: 1px solid var(--border-color); + color: var(--gray-700); + + @media screen and (max-width: 567px) { + align-items: flex-start !important; + } + .col-2 { + @media screen and (max-width: 567px) { + flex: auto; + max-width: 28%; + } + } + + .order-item-name { + font-size: var(--text-base); + font-weight: 500; + } + + .btn:focus, + .btn:hover { + background-color: var(--control-bg); + } + + + .col-6 { + @media screen and (max-width: 567px) { + max-width: 100%; + } + + &.order-item-name { + font-size: var(--text-base); + } + } +} + +.item-grand-total { + font-size: var(--text-base); +} + +.list-item-name, +.item-total, +.order-container, +.order-qty { + font-size: var(--text-md); +} + +.d-s-n { + @media screen and (max-width: 567px) { + display: none; + } +} + +.d-l-n { + @media screen and (min-width: 567px) { + display: none; + } +} + +.border-btm { + border-bottom: 1px solid var(--border-color); +} + +.order-taxes { + display: flex; + + @media screen and (min-width: 567px) { + justify-content: flex-end; + } + + .col-4 { + padding-right: 0; + + .col-8 { + padding-left: 0; + padding-right: 0; + } + + @media screen and (max-width: 567px) { + padding-left: 0; + flex: auto; + max-width: 100%; + } + } +} \ No newline at end of file diff --git a/erpnext/public/scss/website.scss b/erpnext/public/scss/website.scss index 9ea84160342..b5e97f1c34b 100644 --- a/erpnext/public/scss/website.scss +++ b/erpnext/public/scss/website.scss @@ -1,3 +1,4 @@ +@import './order-page'; .filter-options { max-height: 300px; @@ -32,19 +33,29 @@ height: 24px; } -.website-list .result { - margin-top: 2rem; -} +.website-list { + background-color: var(--fg-color); + padding: 0 var(--padding-lg); + border-radius: var(--border-radius-md); -.result { - border-bottom: 1px solid var(--border-color); + @media screen and (max-width: 567px) { + margin-left: -2rem; + } + + &.result { + border-bottom: 1px solid var(--border-color); + } } .transaction-list-item { padding: 1rem 0; - border-top: 1px solid var(--border-color); + border-bottom: 1px solid var(--border-color); position: relative; + &:only-child, &:last-child { + border: 0; + } + a.transaction-item-link { position: absolute; top: 0; @@ -68,3 +79,13 @@ line-height: 1.3; } } + +.list-item-name, .item-total { + font-size: var(--font-size-sm); +} + +.items-preview { + @media screen and (max-width: 567px) { + margin-top: 1rem; + } +} \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index da7576e08de..24375d8252d 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -660,7 +660,7 @@ erpnext.PointOfSale.Controller = class { } else { return; } - } else if (available_qty < qty_needed) { + } else if (is_stock_item && available_qty < qty_needed) { frappe.throw({ message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]), indicator: 'orange' @@ -694,7 +694,7 @@ erpnext.PointOfSale.Controller = class { callback(res) { if (!me.item_stock_map[item_code]) me.item_stock_map[item_code] = {}; - me.item_stock_map[item_code][warehouse] = res.message[0]; + me.item_stock_map[item_code][warehouse] = res.message; } }); } diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index b75ffb235ed..f9b5bb2e452 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -242,13 +242,14 @@ erpnext.PointOfSale.ItemDetails = class { if (this.value) { me.events.form_updated(me.current_item, 'warehouse', this.value).then(() => { me.item_stock_map = me.events.get_item_stock_map(); - const available_qty = me.item_stock_map[me.item_row.item_code] && me.item_stock_map[me.item_row.item_code][this.value]; + const available_qty = me.item_stock_map[me.item_row.item_code][this.value][0]; + const is_stock_item = Boolean(me.item_stock_map[me.item_row.item_code][this.value][1]); if (available_qty === undefined) { me.events.get_available_stock(me.item_row.item_code, this.value).then(() => { // item stock map is updated now reset warehouse me.warehouse_control.set_value(this.value); }) - } else if (available_qty === 0) { + } else if (available_qty === 0 && is_stock_item) { me.warehouse_control.set_value(''); const bold_item_code = me.item_row.item_code.bold(); const bold_warehouse = this.value.bold(); diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 548df318fac..c28f45aed41 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -37,8 +37,10 @@ class Bin(Document): self.set_projected_qty() - self.db_set("reserved_qty_for_production", flt(self.reserved_qty_for_production)) - self.db_set("projected_qty", self.projected_qty) + self.db_set( + "reserved_qty_for_production", flt(self.reserved_qty_for_production), update_modified=True + ) + self.db_set("projected_qty", self.projected_qty, update_modified=True) def update_reserved_qty_for_sub_contracting(self, subcontract_doctype="Subcontracting Order"): # reserved qty @@ -118,9 +120,9 @@ class Bin(Document): else: reserved_qty_for_sub_contract = 0 - self.db_set("reserved_qty_for_sub_contract", reserved_qty_for_sub_contract) + self.db_set("reserved_qty_for_sub_contract", reserved_qty_for_sub_contract, update_modified=True) self.set_projected_qty() - self.db_set("projected_qty", self.projected_qty) + self.db_set("projected_qty", self.projected_qty, update_modified=True) def on_doctype_update(): @@ -193,4 +195,5 @@ def update_qty(bin_name, args): "planned_qty": planned_qty, "projected_qty": projected_qty, }, + update_modified=True, ) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 6bcab737b37..1b9f16814c5 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -6,7 +6,7 @@ import json import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import cstr, flt, nowdate, nowtime +from frappe.utils import add_days, cstr, flt, nowdate, nowtime, today from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.accounts.utils import get_balance_on @@ -1091,6 +1091,36 @@ class TestDeliveryNote(FrappeTestCase): frappe.db.exists("GL Entry", {"voucher_no": dn.name, "voucher_type": dn.doctype}) ) + def test_batch_expiry_for_delivery_note(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + + item = make_item( + "_Test Batch Item For Return Check", + { + "is_purchase_item": 1, + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TBIRC.#####", + }, + ) + + pi = make_purchase_receipt(qty=1, item_code=item.name) + + dn = create_delivery_note(qty=1, item_code=item.name, batch_no=pi.items[0].batch_no) + + dn.load_from_db() + batch_no = dn.items[0].batch_no + self.assertTrue(batch_no) + + frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1)) + + return_dn = make_return_doc(dn.doctype, dn.name) + return_dn.save().submit() + + self.assertTrue(return_dn.docstatus == 1) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") @@ -1117,6 +1147,7 @@ def create_delivery_note(**args): "expense_account": args.expense_account or "Cost of Goods Sold - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC", "serial_no": args.serial_no, + "batch_no": args.batch_no or None, "target_warehouse": args.target_warehouse, }, ) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 7e1476d240a..e61f0f514e3 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -10,6 +10,31 @@ frappe.ui.form.on("Item", { frm.add_fetch('attribute', 'to_range', 'to_range'); frm.add_fetch('attribute', 'increment', 'increment'); frm.add_fetch('tax_type', 'tax_rate', 'tax_rate'); + + frm.make_methods = { + 'Sales Order': () => { + open_form(frm, "Sales Order", "Sales Order Item", "items"); + }, + 'Delivery Note': () => { + open_form(frm, "Delivery Note", "Delivery Note Item", "items"); + }, + 'Sales Invoice': () => { + open_form(frm, "Sales Invoice", "Sales Invoice Item", "items"); + }, + 'Purchase Order': () => { + open_form(frm, "Purchase Order", "Purchase Order Item", "items"); + }, + 'Purchase Receipt': () => { + open_form(frm, "Purchase Receipt", "Purchase Receipt Item", "items"); + }, + 'Purchase Invoice': () => { + open_form(frm, "Purchase Invoice", "Purchase Invoice Item", "items"); + }, + 'Material Request': () => { + open_form(frm, "Material Request", "Material Request Item", "items"); + }, + }; + }, onload: function(frm) { erpnext.item.setup_queries(frm); @@ -858,3 +883,17 @@ frappe.tour['Item'] = [ ]; + +function open_form(frm, doctype, child_doctype, parentfield) { + frappe.model.with_doctype(doctype, () => { + let new_doc = frappe.model.get_new_doc(doctype); + + let new_child_doc = frappe.model.add_child(new_doc, child_doctype, parentfield); + new_child_doc.item_code = frm.doc.name; + new_child_doc.item_name = frm.doc.item_name; + new_child_doc.uom = frm.doc.stock_uom; + new_child_doc.description = frm.doc.description; + + frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); + }); +} diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 143fe408c34..c8bb1b960eb 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -937,17 +937,21 @@ class Item(Document): "Purchase Order Item", "Material Request Item", "Product Bundle", + "BOM", ] for doctype in linked_doctypes: filters = {"item_code": self.name, "docstatus": 1} - if doctype == "Product Bundle": - filters = {"new_item_code": self.name} + if doctype in ("Product Bundle", "BOM"): + if doctype == "Product Bundle": + filters = {"new_item_code": self.name} + fieldname = "new_item_code as docname" + else: + filters = {"item": self.name, "docstatus": 1} + fieldname = "name as docname" - if linked_doc := frappe.db.get_value( - doctype, filters, ["new_item_code as docname"], as_dict=True - ): + if linked_doc := frappe.db.get_value(doctype, filters, fieldname, as_dict=True): return linked_doc.update({"doctype": doctype}) elif doctype in ( diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 1cee553be5b..e35c8bf335e 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -5,6 +5,7 @@ import json import frappe +from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.test_runner import make_test_objects from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, today @@ -816,6 +817,30 @@ class TestItem(FrappeTestCase): item.reload() self.assertEqual(item.is_stock_item, 1) + def test_serach_fields_for_item(self): + from erpnext.controllers.queries import item_query + + make_property_setter("Item", None, "search_fields", "item_name", "Data", for_doctype="Doctype") + + item = make_item(properties={"item_name": "Test Item", "description": "Test Description"}) + data = item_query( + "Item", "Test Item", "", 0, 20, filters={"item_name": "Test Item"}, as_dict=True + ) + self.assertEqual(data[0].name, item.name) + self.assertEqual(data[0].item_name, item.item_name) + self.assertTrue("description" not in data[0]) + + make_property_setter( + "Item", None, "search_fields", "item_name, description", "Data", for_doctype="Doctype" + ) + data = item_query( + "Item", "Test Item", "", 0, 20, filters={"item_name": "Test Item"}, as_dict=True + ) + self.assertEqual(data[0].name, item.name) + self.assertEqual(data[0].item_name, item.item_name) + self.assertEqual(data[0].description, item.description) + self.assertTrue("description" in data[0]) + def set_item_variant_settings(fields): doc = frappe.get_doc("Item Variant Settings") diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index adddb413824..9c1c7e56796 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -183,7 +183,7 @@ class PickList(Document): frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx)) item_code = item.item_code reference = item.sales_order_item or item.material_request_item - key = (item_code, item.uom, item.warehouse, reference) + key = (item_code, item.uom, item.warehouse, item.batch_no, reference) item.idx = None item.name = None diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index b77c3a51348..62697244bab 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1241,6 +1241,37 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(query[0].value, 0) + def test_batch_expiry_for_purchase_receipt(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + item = make_item( + "_Test Batch Item For Return Check", + { + "is_purchase_item": 1, + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TBIRC.#####", + }, + ) + + pi = make_purchase_receipt( + qty=1, + item_code=item.name, + update_stock=True, + ) + + pi.load_from_db() + batch_no = pi.items[0].batch_no + self.assertTrue(batch_no) + + frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(today(), -1)) + + return_pi = make_return_doc(pi.doctype, pi.name) + return_pi.save().submit() + + self.assertTrue(return_pi.docstatus == 1) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index c4705246b3c..d6f9bae5da2 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -128,6 +128,9 @@ def repost(doc): if not frappe.db.exists("Repost Item Valuation", doc.name): return + # This is to avoid TooManyWritesError in case of large reposts + frappe.db.MAX_WRITES_PER_TRANSACTION *= 4 + doc.set_status("In Progress") if not frappe.flags.in_test: frappe.db.commit() diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 738ac330e39..8bcd772d909 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1073,8 +1073,8 @@ class StockEntry(StockController): # No work order could mean independent Manufacture entry, if so skip validation if self.work_order and self.fg_completed_qty > allowed_qty: frappe.throw( - _("For quantity {0} should not be greater than work order quantity {1}").format( - flt(self.fg_completed_qty), wo_qty + _("For quantity {0} should not be greater than allowed quantity {1}").format( + flt(self.fg_completed_qty), allowed_qty ) ) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 329cd7da09b..c64370dcdf2 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -153,6 +153,11 @@ class StockLedgerEntry(Document): def validate_batch(self): if self.batch_no and self.voucher_type != "Stock Entry": + if (self.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and self.actual_qty < 0) or ( + self.voucher_type in ["Delivery Note", "Sales Invoice"] and self.actual_qty > 0 + ): + return + expiry_date = frappe.db.get_value("Batch", self.batch_no, "expiry_date") if expiry_date: if getdate(self.posting_date) > getdate(expiry_date): diff --git a/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py b/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py index 7a1b8c0cee9..0ec4e1ce957 100644 --- a/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py +++ b/erpnext/stock/report/delivery_note_trends/delivery_note_trends.py @@ -45,4 +45,5 @@ def get_chart_data(data, filters): "datasets": [{"name": _("Total Delivered Amount"), "values": datapoints}], }, "type": "bar", + "fieldtype": "Currency", } diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py index 23e3c8a97f5..df01b14d11a 100644 --- a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py +++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py @@ -4,6 +4,8 @@ import frappe from frappe import _ +from frappe.query_builder import Field +from frappe.query_builder.functions import Min, Timestamp from frappe.utils import add_days, getdate, today import erpnext @@ -28,7 +30,7 @@ def execute(filters=None): def get_unsync_date(filters): date = filters.from_date if not date: - date = frappe.db.sql(""" SELECT min(posting_date) from `tabStock Ledger Entry`""") + date = (frappe.qb.from_("Stock Ledger Entry").select(Min(Field("posting_date")))).run() date = date[0][0] if not date: @@ -54,22 +56,27 @@ def get_data(report_filters): result = [] voucher_wise_dict = {} - data = frappe.db.sql( - """ - SELECT - name, posting_date, posting_time, voucher_type, voucher_no, - stock_value_difference, stock_value, warehouse, item_code - FROM - `tabStock Ledger Entry` - WHERE - posting_date - = %s and company = %s - and is_cancelled = 0 - ORDER BY timestamp(posting_date, posting_time) asc, creation asc - """, - (from_date, report_filters.company), - as_dict=1, - ) + sle = frappe.qb.DocType("Stock Ledger Entry") + data = ( + frappe.qb.from_(sle) + .select( + sle.name, + sle.posting_date, + sle.posting_time, + sle.voucher_type, + sle.voucher_no, + sle.stock_value_difference, + sle.stock_value, + sle.warehouse, + sle.item_code, + ) + .where( + (sle.posting_date == from_date) + & (sle.company == report_filters.company) + & (sle.is_cancelled == 0) + ) + .orderby(Timestamp(sle.posting_date, sle.posting_time), sle.creation) + ).run(as_dict=True) for d in data: voucher_wise_dict.setdefault((d.item_code, d.warehouse), []).append(d) diff --git a/erpnext/stock/report/item_price_stock/item_price_stock.py b/erpnext/stock/report/item_price_stock/item_price_stock.py index 15218e63a87..1b07f596c7b 100644 --- a/erpnext/stock/report/item_price_stock/item_price_stock.py +++ b/erpnext/stock/report/item_price_stock/item_price_stock.py @@ -62,22 +62,28 @@ def get_data(filters, columns): def get_item_price_qty_data(filters): - conditions = "" - if filters.get("item_code"): - conditions += "where a.item_code=%(item_code)s" + item_price = frappe.qb.DocType("Item Price") + bin = frappe.qb.DocType("Bin") - item_results = frappe.db.sql( - """select a.item_code, a.item_name, a.name as price_list_name, - a.brand as brand, b.warehouse as warehouse, b.actual_qty as actual_qty - from `tabItem Price` a left join `tabBin` b - ON a.item_code = b.item_code - {conditions}""".format( - conditions=conditions - ), - filters, - as_dict=1, + query = ( + frappe.qb.from_(item_price) + .left_join(bin) + .on(item_price.item_code == bin.item_code) + .select( + item_price.item_code, + item_price.item_name, + item_price.name.as_("price_list_name"), + item_price.brand.as_("brand"), + bin.warehouse.as_("warehouse"), + bin.actual_qty.as_("actual_qty"), + ) ) + if filters.get("item_code"): + query = query.where(item_price.item_code == filters.get("item_code")) + + item_results = query.run(as_dict=True) + price_list_names = list(set(item.price_list_name for item in item_results)) buying_price_map = get_price_map(price_list_names, buying=1) diff --git a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py index fe2d55a3913..b62a6ee6fd8 100644 --- a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py +++ b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py @@ -46,4 +46,5 @@ def get_chart_data(data, filters): }, "type": "bar", "colors": ["#5e64ff"], + "fieldtype": "Currency", } diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 50309647de0..9ca40c3675f 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1053,7 +1053,7 @@ class update_entries_after(object): updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value} if data.valuation_rate is not None: updated_values["valuation_rate"] = data.valuation_rate - frappe.db.set_value("Bin", bin_name, updated_values) + frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True) def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False): diff --git a/erpnext/templates/includes/footer/footer_extension.html b/erpnext/templates/includes/footer/footer_extension.html index c7f0d06dff2..0072dc280c7 100644 --- a/erpnext/templates/includes/footer/footer_extension.html +++ b/erpnext/templates/includes/footer/footer_extension.html @@ -6,7 +6,7 @@ aria-label="{{ _('Your email address...') }}" aria-describedby="footer-subscribe-button">
Some functionality is disabled for the demo and the data will be cleared regularly.
- -+ {{ _("Valid Till") }}: {{ frappe.utils.format_date(doc.valid_till, 'medium') }} +
{% endif %} - +- {{ _("Valid Till") }}: {{ frappe.utils.format_date(doc.valid_till, 'medium') }} -
- {% endif %} -
- {%- set party_name = doc.supplier_name if doc.doctype in ['Supplier Quotation', 'Purchase Invoice', 'Purchase Order'] else doc.customer_name %}
- {{ party_name }}
-
- {% if doc.contact_display and doc.contact_display != party_name %}
-
- {{ doc.contact_display }}
- {% endif %}
-
| - {{ _("Item") }} - | -- {{ _("Quantity") }} - | -- {{ _("Amount") }} - | - - - {% for d in doc.items %} -
|---|---|---|
| - {{ item_name_and_description(d) }} - | -
- {{ d.qty }}
- {% if d.delivered_qty is defined and d.delivered_qty != None %}
- {{ _("Delivered") }} {{ d.delivered_qty }} +
+
+
+ {% if doc.doctype == "Quotation" and not doc.docstatus %}
+ {{ _("Pending") }}
+ {% else %}
+ {{ _(doc.get('indicator_title')) or _(doc.status) or _("Submitted") }}
{% endif %}
- |
-
- {{ d.get_formatted("amount") }}
- {{ _("Rate:") }} {{ d.get_formatted("rate") }} - |
-
Available Points: {{ available_loyalty_points }}
+Available Points: {{ + available_loyalty_points }}
{{ doc.terms }}
+{{ doc.terms }}
Select the date and your timezone
+{{ _("Select the date and your timezone") }}
Selected date is at
+ {{ _("Selected date is") }} {{ _("at") }}
{{ _("Add details") }}
+