Merge pull request #36596 from Nihantra-Patel/pos_receipt_mail
fix: POS Invoice Email Receipt Mail
diff --git a/.mergify.yml b/.mergify.yml
index c5f3d83..804b27d 100644
--- a/.mergify.yml
+++ b/.mergify.yml
@@ -15,6 +15,8 @@
- or:
- base=version-13
- base=version-12
+ - base=version-14
+ - base=version-15
actions:
close:
comment:
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.json b/erpnext/accounts/doctype/gl_entry/gl_entry.json
index e6d97a1..5063ec6 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.json
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.json
@@ -32,7 +32,11 @@
"finance_book",
"to_rename",
"due_date",
- "is_cancelled"
+ "is_cancelled",
+ "transaction_currency",
+ "debit_in_transaction_currency",
+ "credit_in_transaction_currency",
+ "transaction_exchange_rate"
],
"fields": [
{
@@ -253,15 +257,40 @@
"fieldname": "is_cancelled",
"fieldtype": "Check",
"label": "Is Cancelled"
+ },
+ {
+ "fieldname": "transaction_currency",
+ "fieldtype": "Link",
+ "label": "Transaction Currency",
+ "options": "Currency"
+ },
+ {
+ "fieldname": "transaction_exchange_rate",
+ "fieldtype": "Float",
+ "label": "Transaction Exchange Rate"
+ },
+ {
+ "fieldname": "debit_in_transaction_currency",
+ "fieldtype": "Currency",
+ "label": "Debit Amount in Transaction Currency",
+ "options": "transaction_currency"
+ },
+ {
+ "fieldname": "credit_in_transaction_currency",
+ "fieldtype": "Currency",
+ "label": "Credit Amount in Transaction Currency",
+ "options": "transaction_currency"
}
],
"icon": "fa fa-list",
"idx": 1,
"in_create": 1,
- "modified": "2020-04-07 16:22:33.766994",
+ "links": [],
+ "modified": "2023-08-16 21:38:44.072267",
"modified_by": "Administrator",
"module": "Accounts",
"name": "GL Entry",
+ "naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -290,5 +319,6 @@
"quick_entry": 1,
"search_fields": "voucher_no,account,posting_date,against_voucher",
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json
index 80df0ff..2eb54a5 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.json
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json
@@ -89,7 +89,7 @@
"label": "Entry Type",
"oldfieldname": "voucher_type",
"oldfieldtype": "Select",
- "options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nExchange Rate Revaluation\nExchange Gain Or Loss\nDeferred Revenue\nDeferred Expense\nReversal Of ITC",
+ "options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nExchange Rate Revaluation\nExchange Gain Or Loss\nDeferred Revenue\nDeferred Expense",
"reqd": 1,
"search_index": 1
},
@@ -555,7 +555,45 @@
"name": "Journal Entry",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
- "permissions": [],
+ "permissions": [
+ {
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "import": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Auditor"
+ }
+ ],
"search_fields": "voucher_type,posting_date, due_date, cheque_no",
"sort_field": "modified",
"sort_order": "DESC",
diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py
index a134f74..4f58579 100644
--- a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py
+++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py
@@ -145,8 +145,8 @@
loyalty_amount = flt(points_to_redeem * loyalty_program_details.conversion_factor)
- if loyalty_amount > ref_doc.grand_total:
- frappe.throw(_("You can't redeem Loyalty Points having more value than the Grand Total."))
+ if loyalty_amount > ref_doc.rounded_total:
+ frappe.throw(_("You can't redeem Loyalty Points having more value than the Rounded Total."))
if not ref_doc.loyalty_amount and ref_doc.loyalty_amount != loyalty_amount:
ref_doc.loyalty_amount = loyalty_amount
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 64b4d16..ac31e8a 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -71,7 +71,7 @@
self.setup_party_account_field()
self.set_missing_values()
self.set_liability_account()
- self.set_missing_ref_details()
+ self.set_missing_ref_details(force=True)
self.validate_payment_type()
self.validate_party_details()
self.set_exchange_rate()
@@ -230,84 +230,88 @@
return False
def validate_allocated_amount_with_latest_data(self):
- latest_references = get_outstanding_reference_documents(
- {
- "posting_date": self.posting_date,
- "company": self.company,
- "party_type": self.party_type,
- "payment_type": self.payment_type,
- "party": self.party,
- "party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
- "get_outstanding_invoices": True,
- "get_orders_to_be_billed": True,
- },
- validate=True,
- )
+ if self.references:
+ uniq_vouchers = set([(x.reference_doctype, x.reference_name) for x in self.references])
+ vouchers = [frappe._dict({"voucher_type": x[0], "voucher_no": x[1]}) for x in uniq_vouchers]
+ latest_references = get_outstanding_reference_documents(
+ {
+ "posting_date": self.posting_date,
+ "company": self.company,
+ "party_type": self.party_type,
+ "payment_type": self.payment_type,
+ "party": self.party,
+ "party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
+ "get_outstanding_invoices": True,
+ "get_orders_to_be_billed": True,
+ "vouchers": vouchers,
+ },
+ validate=True,
+ )
- # Group latest_references by (voucher_type, voucher_no)
- latest_lookup = {}
- for d in latest_references:
- d = frappe._dict(d)
- latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d
+ # Group latest_references by (voucher_type, voucher_no)
+ latest_lookup = {}
+ for d in latest_references:
+ d = frappe._dict(d)
+ latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d
- for idx, d in enumerate(self.get("references"), start=1):
- latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()
+ for idx, d in enumerate(self.get("references"), start=1):
+ latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()
- # If term based allocation is enabled, throw
- if (
- d.payment_term is None or d.payment_term == ""
- ) and self.term_based_allocation_enabled_for_reference(
- d.reference_doctype, d.reference_name
- ):
- frappe.throw(
- _(
- "{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section"
- ).format(frappe.bold(d.reference_name), frappe.bold(idx))
- )
-
- # if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key
- latest = latest.get(d.payment_term) or latest.get(None)
-
- # The reference has already been fully paid
- if not latest:
- frappe.throw(
- _("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
- )
- # The reference has already been partly paid
- elif latest.outstanding_amount < latest.invoice_amount and flt(
- d.outstanding_amount, d.precision("outstanding_amount")
- ) != flt(latest.outstanding_amount, d.precision("outstanding_amount")):
- frappe.throw(
- _(
- "{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
- ).format(_(d.reference_doctype), d.reference_name)
- )
-
- fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
-
- if (
- d.payment_term
- and (
- (flt(d.allocated_amount)) > 0
- and latest.payment_term_outstanding
- and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding))
- )
- and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name)
- ):
- frappe.throw(
- _(
- "Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}"
- ).format(
- d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term
+ # If term based allocation is enabled, throw
+ if (
+ d.payment_term is None or d.payment_term == ""
+ ) and self.term_based_allocation_enabled_for_reference(
+ d.reference_doctype, d.reference_name
+ ):
+ frappe.throw(
+ _(
+ "{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section"
+ ).format(frappe.bold(d.reference_name), frappe.bold(idx))
)
- )
- if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
- frappe.throw(fail_message.format(d.idx))
+ # if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key
+ latest = latest.get(d.payment_term) or latest.get(None)
- # Check for negative outstanding invoices as well
- if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
- frappe.throw(fail_message.format(d.idx))
+ # The reference has already been fully paid
+ if not latest:
+ frappe.throw(
+ _("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
+ )
+ # The reference has already been partly paid
+ elif latest.outstanding_amount < latest.invoice_amount and flt(
+ d.outstanding_amount, d.precision("outstanding_amount")
+ ) != flt(latest.outstanding_amount, d.precision("outstanding_amount")):
+ frappe.throw(
+ _(
+ "{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
+ ).format(_(d.reference_doctype), d.reference_name)
+ )
+
+ fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
+
+ if (
+ d.payment_term
+ and (
+ (flt(d.allocated_amount)) > 0
+ and latest.payment_term_outstanding
+ and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding))
+ )
+ and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name)
+ ):
+ frappe.throw(
+ _(
+ "Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}"
+ ).format(
+ d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term
+ )
+ )
+
+ if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
+ frappe.throw(fail_message.format(d.idx))
+
+ # Check for negative outstanding invoices as well
+ if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
+ frappe.throw(fail_message.format(d.idx))
def delink_advance_entry_references(self):
for reference in self.references:
@@ -1587,6 +1591,7 @@
min_outstanding=args.get("outstanding_amt_greater_than"),
max_outstanding=args.get("outstanding_amt_less_than"),
accounting_dimensions=accounting_dimensions_filter,
+ vouchers=args.get("vouchers") or None,
)
outstanding_invoices = split_invoices_based_on_payment_terms(
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index 8f9f7ce..c8bf664 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -702,7 +702,50 @@
pe2.submit()
# create return entry against si1
- create_sales_invoice(is_return=1, return_against=si1.name, qty=-1)
+ cr_note = create_sales_invoice(is_return=1, return_against=si1.name, qty=-1)
+ si1_outstanding = frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount")
+
+ # create JE(credit note) manually against si1 and cr_note
+ je = frappe.get_doc(
+ {
+ "doctype": "Journal Entry",
+ "company": si1.company,
+ "voucher_type": "Credit Note",
+ "posting_date": nowdate(),
+ }
+ )
+ je.append(
+ "accounts",
+ {
+ "account": si1.debit_to,
+ "party_type": "Customer",
+ "party": si1.customer,
+ "debit": 0,
+ "credit": 100,
+ "debit_in_account_currency": 0,
+ "credit_in_account_currency": 100,
+ "reference_type": si1.doctype,
+ "reference_name": si1.name,
+ "cost_center": si1.items[0].cost_center,
+ },
+ )
+ je.append(
+ "accounts",
+ {
+ "account": cr_note.debit_to,
+ "party_type": "Customer",
+ "party": cr_note.customer,
+ "debit": 100,
+ "credit": 0,
+ "debit_in_account_currency": 100,
+ "credit_in_account_currency": 0,
+ "reference_type": cr_note.doctype,
+ "reference_name": cr_note.name,
+ "cost_center": cr_note.items[0].cost_center,
+ },
+ )
+ je.save().submit()
+
si1_outstanding = frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount")
self.assertEqual(si1_outstanding, -100)
diff --git a/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py b/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py
index fc6dbba..ce9579e 100644
--- a/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py
+++ b/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py
@@ -294,7 +294,7 @@
cr_note1.return_against = si3.name
cr_note1 = cr_note1.save().submit()
- pl_entries = (
+ pl_entries_si3 = (
qb.from_(ple)
.select(
ple.voucher_type,
@@ -309,7 +309,24 @@
.run(as_dict=True)
)
- expected_values = [
+ pl_entries_cr_note1 = (
+ qb.from_(ple)
+ .select(
+ ple.voucher_type,
+ ple.voucher_no,
+ ple.against_voucher_type,
+ ple.against_voucher_no,
+ ple.amount,
+ ple.delinked,
+ )
+ .where(
+ (ple.against_voucher_type == cr_note1.doctype) & (ple.against_voucher_no == cr_note1.name)
+ )
+ .orderby(ple.creation)
+ .run(as_dict=True)
+ )
+
+ expected_values_for_si3 = [
{
"voucher_type": si3.doctype,
"voucher_no": si3.name,
@@ -317,18 +334,21 @@
"against_voucher_no": si3.name,
"amount": amount,
"delinked": 0,
- },
+ }
+ ]
+ # credit/debit notes post ledger entries against itself
+ expected_values_for_cr_note1 = [
{
"voucher_type": cr_note1.doctype,
"voucher_no": cr_note1.name,
- "against_voucher_type": si3.doctype,
- "against_voucher_no": si3.name,
+ "against_voucher_type": cr_note1.doctype,
+ "against_voucher_no": cr_note1.name,
"amount": -amount,
"delinked": 0,
},
]
- self.assertEqual(pl_entries[0], expected_values[0])
- self.assertEqual(pl_entries[1], expected_values[1])
+ self.assertEqual(pl_entries_si3, expected_values_for_si3)
+ self.assertEqual(pl_entries_cr_note1, expected_values_for_cr_note1)
def test_je_against_inv_and_note(self):
ple = self.ple
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
index 2adc123..7b7ce7a 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
@@ -163,6 +163,15 @@
this.frm.refresh();
}
+ invoice_name() {
+ this.frm.trigger("get_unreconciled_entries");
+ }
+
+ payment_name() {
+ this.frm.trigger("get_unreconciled_entries");
+ }
+
+
clear_child_tables() {
this.frm.clear_table("invoices");
this.frm.clear_table("payments");
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
index 5f6c703..b88791d 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
@@ -27,8 +27,10 @@
"bank_cash_account",
"cost_center",
"sec_break1",
+ "invoice_name",
"invoices",
"column_break_15",
+ "payment_name",
"payments",
"sec_break2",
"allocation"
@@ -137,6 +139,7 @@
"label": "Minimum Invoice Amount"
},
{
+ "default": "50",
"description": "System will fetch all the entries if limit value is zero.",
"fieldname": "invoice_limit",
"fieldtype": "Int",
@@ -167,6 +170,7 @@
"label": "Maximum Payment Amount"
},
{
+ "default": "50",
"description": "System will fetch all the entries if limit value is zero.",
"fieldname": "payment_limit",
"fieldtype": "Int",
@@ -194,13 +198,23 @@
"label": "Default Advance Account",
"mandatory_depends_on": "doc.party_type",
"options": "Account"
+ },
+ {
+ "fieldname": "invoice_name",
+ "fieldtype": "Data",
+ "label": "Filter on Invoice"
+ },
+ {
+ "fieldname": "payment_name",
+ "fieldtype": "Data",
+ "label": "Filter on Payment"
}
],
"hide_toolbar": 1,
"icon": "icon-resize-horizontal",
"issingle": 1,
"links": [],
- "modified": "2023-06-09 13:02:48.718362",
+ "modified": "2023-08-15 05:35:50.109290",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation",
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 3a9e80a..7ef5278 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -5,6 +5,7 @@
import frappe
from frappe import _, msgprint, qb
from frappe.model.document import Document
+from frappe.query_builder import Criterion
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
@@ -74,6 +75,9 @@
}
)
+ if self.payment_name:
+ condition.update({"name": self.payment_name})
+
payment_entries = get_advance_payment_entries(
self.party_type,
self.party,
@@ -89,6 +93,9 @@
def get_jv_entries(self):
condition = self.get_conditions()
+ if self.payment_name:
+ condition += f" and t1.name like '%%{self.payment_name}%%'"
+
if self.get("cost_center"):
condition += f" and t2.cost_center = '{self.cost_center}' "
@@ -146,6 +153,15 @@
def get_return_invoices(self):
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
doc = qb.DocType(voucher_type)
+
+ conditions = []
+ conditions.append(doc.docstatus == 1)
+ conditions.append(doc[frappe.scrub(self.party_type)] == self.party)
+ conditions.append(doc.is_return == 1)
+
+ if self.payment_name:
+ conditions.append(doc.name.like(f"%{self.payment_name}%"))
+
self.return_invoices = (
qb.from_(doc)
.select(
@@ -153,11 +169,7 @@
doc.name.as_("voucher_no"),
doc.return_against,
)
- .where(
- (doc.docstatus == 1)
- & (doc[frappe.scrub(self.party_type)] == self.party)
- & (doc.is_return == 1)
- )
+ .where(Criterion.all(conditions))
.run(as_dict=True)
)
@@ -174,15 +186,12 @@
self.common_filter_conditions.append(ple.account == self.receivable_payable_account)
self.get_return_invoices()
- return_invoices = [
- x for x in self.return_invoices if x.return_against == None or x.return_against == ""
- ]
outstanding_dr_or_cr = []
- if return_invoices:
+ if self.return_invoices:
ple_query = QueryPaymentLedger()
return_outstanding = ple_query.get_voucher_outstandings(
- vouchers=return_invoices,
+ vouchers=self.return_invoices,
common_filter=self.common_filter_conditions,
posting_date=self.ple_posting_date_filter,
min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None,
@@ -226,6 +235,8 @@
min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None,
max_outstanding=self.maximum_invoice_amount if self.maximum_invoice_amount else None,
accounting_dimensions=self.accounting_dimension_filter_conditions,
+ limit=self.invoice_limit,
+ voucher_no=self.invoice_name,
)
cr_dr_notes = (
@@ -401,59 +412,6 @@
self.get_unreconciled_entries()
- def make_difference_entry(self, row):
- journal_entry = frappe.new_doc("Journal Entry")
- journal_entry.voucher_type = "Exchange Gain Or Loss"
- journal_entry.company = self.company
- journal_entry.posting_date = nowdate()
- journal_entry.multi_currency = 1
-
- party_account_currency = frappe.get_cached_value(
- "Account", self.receivable_payable_account, "account_currency"
- )
- difference_account_currency = frappe.get_cached_value(
- "Account", row.difference_account, "account_currency"
- )
-
- # Account Currency has balance
- dr_or_cr = "debit" if self.party_type == "Customer" else "credit"
- reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
-
- journal_account = frappe._dict(
- {
- "account": self.receivable_payable_account,
- "party_type": self.party_type,
- "party": self.party,
- "account_currency": party_account_currency,
- "exchange_rate": 0,
- "cost_center": erpnext.get_default_cost_center(self.company),
- "reference_type": row.against_voucher_type,
- "reference_name": row.against_voucher,
- dr_or_cr: flt(row.difference_amount),
- dr_or_cr + "_in_account_currency": 0,
- }
- )
-
- journal_entry.append("accounts", journal_account)
-
- journal_account = frappe._dict(
- {
- "account": row.difference_account,
- "account_currency": difference_account_currency,
- "exchange_rate": 1,
- "cost_center": erpnext.get_default_cost_center(self.company),
- reverse_dr_or_cr + "_in_account_currency": flt(row.difference_amount),
- reverse_dr_or_cr: flt(row.difference_amount),
- }
- )
-
- journal_entry.append("accounts", journal_account)
-
- journal_entry.save()
- journal_entry.submit()
-
- return journal_entry
-
def get_payment_details(self, row, dr_or_cr):
return frappe._dict(
{
@@ -619,16 +577,6 @@
def reconcile_dr_cr_note(dr_cr_notes, company):
- def get_difference_row(inv):
- if inv.difference_amount != 0 and inv.difference_account:
- difference_row = {
- "account": inv.difference_account,
- inv.dr_or_cr: abs(inv.difference_amount) if inv.difference_amount > 0 else 0,
- reconcile_dr_or_cr: abs(inv.difference_amount) if inv.difference_amount < 0 else 0,
- "cost_center": erpnext.get_default_cost_center(company),
- }
- return difference_row
-
for inv in dr_cr_notes:
voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
index a6c0102..faceaf3 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
@@ -153,7 +153,7 @@
frappe.ui.form.on('POS Closing Entry Detail', {
closing_amount: (frm, cdt, cdn) => {
const row = locals[cdt][cdn];
- frappe.model.set_value(cdt, cdn, "difference", flt(row.expected_amount - row.closing_amount));
+ frappe.model.set_value(cdt, cdn, "difference", flt(row.closing_amount - row.expected_amount));
}
})
@@ -185,6 +185,7 @@
}
if (payment) {
payment.expected_amount += flt(p.amount);
+ payment.closing_amount = payment.expected_amount;
payment.difference = payment.closing_amount - payment.expected_amount;
} else {
frm.add_child("payment_reconciliation", {
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json
index 9d15e6c..a98a24c 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json
@@ -221,6 +221,7 @@
"read_only": 1
},
{
+ "default": "Now",
"fieldname": "posting_time",
"fieldtype": "Time",
"label": "Posting Time",
@@ -235,7 +236,7 @@
"link_fieldname": "pos_closing_entry"
}
],
- "modified": "2022-08-01 11:37:14.991228",
+ "modified": "2023-08-10 16:25:49.322697",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Closing Entry",
diff --git a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py
index 1deb3c5..93ba90a 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py
+++ b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py
@@ -8,9 +8,11 @@
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import (
make_closing_entry_from_opening,
)
+from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
+from erpnext.selling.page.point_of_sale.point_of_sale import get_items
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
@@ -67,6 +69,36 @@
self.assertTrue(pcv_doc.name)
+ def test_pos_qty_for_item(self):
+ """
+ Test if quantity is calculated correctly for an item in POS Closing Entry
+ """
+ test_user, pos_profile = init_user_and_profile()
+ opening_entry = create_opening_entry(pos_profile, test_user.name)
+
+ test_item_qty = get_test_item_qty(pos_profile)
+
+ pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
+ pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
+ pos_inv1.submit()
+
+ pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
+ pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200})
+ pos_inv2.submit()
+
+ # make return entry of pos_inv2
+ pos_return = make_sales_return(pos_inv2.name)
+ pos_return.paid_amount = pos_return.grand_total
+ pos_return.save()
+ pos_return.submit()
+
+ pcv_doc = make_closing_entry_from_opening(opening_entry)
+ pcv_doc.submit()
+
+ opening_entry = create_opening_entry(pos_profile, test_user.name)
+ test_item_qty_after_sales = get_test_item_qty(pos_profile)
+ self.assertEqual(test_item_qty_after_sales, test_item_qty - 1)
+
def test_cancelling_of_pos_closing_entry(self):
test_user, pos_profile = init_user_and_profile()
opening_entry = create_opening_entry(pos_profile, test_user.name)
@@ -123,3 +155,19 @@
pos_profile.save()
return test_user, pos_profile
+
+
+def get_test_item_qty(pos_profile):
+ test_item_pos = get_items(
+ start=0,
+ page_length=5,
+ price_list="Standard Selling",
+ pos_profile=pos_profile.name,
+ search_term="_Test Item",
+ item_group="All Item Groups",
+ )
+
+ test_item_qty = [item for item in test_item_pos["items"] if item["item_code"] == "_Test Item"][
+ 0
+ ].get("actual_qty")
+ return test_item_qty
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
index 6f0b801..ae132eb 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
@@ -131,6 +131,7 @@
args: { "pos_profile": frm.pos_profile },
callback: ({ message: profile }) => {
this.update_customer_groups_settings(profile?.customer_groups);
+ this.frm.set_value("company", profile?.company);
},
});
}
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 4b2fcec..842f159 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -49,6 +49,7 @@
self.validate_pos()
self.validate_payment_amount()
self.validate_loyalty_transaction()
+ self.validate_company_with_pos_company()
if self.coupon_code:
from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
@@ -281,6 +282,14 @@
if total_amount_in_payments and total_amount_in_payments < invoice_total:
frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total))
+ def validate_company_with_pos_company(self):
+ if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"):
+ frappe.throw(
+ _("Company {} does not match with POS Profile Company {}").format(
+ self.company, frappe.db.get_value("POS Profile", self.pos_profile, "company")
+ )
+ )
+
def validate_loyalty_transaction(self):
if self.redeem_loyalty_points and (
not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center
@@ -359,6 +368,7 @@
profile = {}
if self.pos_profile:
profile = frappe.get_doc("POS Profile", self.pos_profile)
+ self.company = profile.get("company")
if not self.get("payments") and not for_validate:
update_multi_mode_option(self, profile)
@@ -542,6 +552,7 @@
is_stock_item = True
bin_qty = get_bin_qty(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 = True
@@ -595,7 +606,6 @@
.where(
(p_inv.name == p_item.parent)
& (IfNull(p_inv.consolidated_invoice, "") == "")
- & (p_inv.is_return == 0)
& (p_item.docstatus == 1)
& (p_item.item_code == item_code)
& (p_item.warehouse == warehouse)
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
index d8cbcc1..b587ce6 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
@@ -95,7 +95,6 @@
sales_invoice = self.process_merging_into_sales_invoice(sales)
self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log
-
self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note)
def on_cancel(self):
@@ -108,7 +107,6 @@
def process_merging_into_sales_invoice(self, data):
sales_invoice = self.get_new_sales_invoice()
-
sales_invoice = self.merge_pos_invoice_into(sales_invoice, data)
sales_invoice.is_consolidated = 1
@@ -165,8 +163,7 @@
for i in items:
if (
i.item_code == item.item_code
- and not i.serial_no
- and not i.batch_no
+ and not i.serial_and_batch_bundle
and i.uom == item.uom
and i.net_rate == item.net_rate
and i.warehouse == item.warehouse
@@ -385,6 +382,7 @@
for d in invoices
if d.is_return and d.return_against
]
+
for pos_invoice in pos_return_docs:
for item in pos_invoice.items:
if not item.serial_no and not item.serial_and_batch_bundle:
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 66438a7..efe9741 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -86,8 +86,7 @@
}
}
- if(doc.docstatus == 1 && doc.outstanding_amount != 0
- && !(doc.is_return && doc.return_against) && !doc.on_hold) {
+ if(doc.docstatus == 1 && doc.outstanding_amount != 0 && !doc.on_hold) {
this.frm.add_custom_button(
__('Payment'),
() => this.make_payment_entry(),
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index f334399..9f1224d 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -628,9 +628,7 @@
"credit_in_account_currency": base_grand_total
if self.party_account_currency == self.company_currency
else grand_total,
- "against_voucher": self.return_against
- if cint(self.is_return) and self.return_against
- else self.name,
+ "against_voucher": self.name,
"against_voucher_type": self.doctype,
"project": self.project,
"cost_center": self.cost_center,
@@ -1644,12 +1642,8 @@
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
self.status = "Unpaid"
# Check if outstanding amount is 0 due to debit note issued against invoice
- elif (
- outstanding_amount <= 0
- and self.is_return == 0
- and frappe.db.get_value(
- "Purchase Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1}
- )
+ elif self.is_return == 0 and frappe.db.get_value(
+ "Purchase Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1}
):
self.status = "Debit Note Issued"
elif self.is_return == 1:
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index a4bcdb4..642e99c 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -98,8 +98,7 @@
erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
}
- if (doc.docstatus == 1 && doc.outstanding_amount!=0
- && !(cint(doc.is_return) && doc.return_against)) {
+ if (doc.docstatus == 1 && doc.outstanding_amount!=0) {
this.frm.add_custom_button(
__('Payment'),
() => this.make_payment_entry(),
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 7581366..e5adeae 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -716,6 +716,7 @@
"fieldtype": "Table",
"hide_days": 1,
"hide_seconds": 1,
+ "label": "Items",
"oldfieldname": "entries",
"oldfieldtype": "Table",
"options": "Sales Invoice Item",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 0bc5aa2..fba2fa7 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -1104,9 +1104,7 @@
"debit_in_account_currency": base_grand_total
if self.party_account_currency == self.company_currency
else grand_total,
- "against_voucher": self.return_against
- if cint(self.is_return) and self.return_against
- else self.name,
+ "against_voucher": self.name,
"against_voucher_type": self.doctype,
"cost_center": self.cost_center,
"project": self.project,
@@ -1732,12 +1730,8 @@
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
self.status = "Unpaid"
# Check if outstanding amount is 0 due to credit note issued against invoice
- elif (
- outstanding_amount <= 0
- and self.is_return == 0
- and frappe.db.get_value(
- "Sales Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1}
- )
+ elif self.is_return == 0 and frappe.db.get_value(
+ "Sales Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1}
):
self.status = "Credit Note Issued"
elif self.is_return == 1:
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 63c0c45..21b39d7 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1500,8 +1500,8 @@
self.assertEqual(party_credited, 1000)
# Check outstanding amount
- self.assertFalse(si1.outstanding_amount)
- self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 1500)
+ self.assertEqual(frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount"), -1000)
+ self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 2500)
def test_gle_made_when_asset_is_returned(self):
create_asset_data()
@@ -3376,6 +3376,7 @@
set_advance_flag(company="_Test Company", flag=0, default_account="")
+ @change_settings("Selling Settings", {"allow_negative_rates_for_items": 0})
def test_sales_return_negative_rate(self):
si = create_sales_invoice(is_return=1, qty=-2, rate=-10, do_not_save=True)
self.assertRaises(frappe.ValidationError, si.save)
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 e66a886..954b4e7 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -100,11 +100,14 @@
tax_details = get_tax_withholding_details(tax_withholding_category, posting_date, inv.company)
if not tax_details:
- frappe.throw(
- _("Please set associated account in Tax Withholding Category {0} against Company {1}").format(
- tax_withholding_category, inv.company
- )
+ frappe.msgprint(
+ _(
+ "Skipping Tax Withholding Category {0} as there is no associated account set for Company {1} in it."
+ ).format(tax_withholding_category, inv.company)
)
+ if inv.doctype == "Purchase Invoice":
+ return {}, [], {}
+ return {}
if party_type == "Customer" and not tax_details.cumulative_threshold:
# TCS is only chargeable on sum of invoiced value
@@ -262,14 +265,20 @@
if tax_deducted:
net_total = inv.tax_withholding_net_total
if ldc:
- tax_amount = get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total)
+ limit_consumed = get_limit_consumed(ldc, parties)
+ if is_valid_certificate(ldc, posting_date, limit_consumed):
+ tax_amount = get_lower_deduction_amount(
+ net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details
+ )
+ else:
+ tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
else:
tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
# once tds is deducted, not need to add vouchers in the invoice
voucher_wise_amount = {}
else:
- tax_amount = get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers)
+ tax_amount = get_tds_amount(ldc, parties, inv, tax_details, vouchers)
elif party_type == "Customer":
if tax_deducted:
@@ -416,7 +425,7 @@
return sum(entries)
-def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
+def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
tds_amount = 0
invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1}
@@ -496,15 +505,10 @@
net_total += inv.tax_withholding_net_total
supp_credit_amt = net_total - cumulative_threshold
- if ldc and is_valid_certificate(
- ldc.valid_from,
- ldc.valid_upto,
- inv.get("posting_date") or inv.get("transaction_date"),
- tax_deducted,
- inv.tax_withholding_net_total,
- ldc.certificate_limit,
- ):
- tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details)
+ if ldc and is_valid_certificate(ldc, inv.get("posting_date") or inv.get("transaction_date"), 0):
+ tds_amount = get_lower_deduction_amount(
+ supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details
+ )
else:
tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0
@@ -582,8 +586,7 @@
return inv.grand_total - tcs_tax_row_amount
-def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total):
- tds_amount = 0
+def get_limit_consumed(ldc, parties):
limit_consumed = frappe.db.get_value(
"Purchase Invoice",
{
@@ -597,37 +600,29 @@
"sum(tax_withholding_net_total)",
)
- if is_valid_certificate(
- ldc.valid_from, ldc.valid_upto, posting_date, limit_consumed, net_total, ldc.certificate_limit
- ):
- tds_amount = get_ltds_amount(
- net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details
- )
-
- return tds_amount
+ return limit_consumed
-def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details):
- if certificate_limit - flt(deducted_amount) - flt(current_amount) >= 0:
+def get_lower_deduction_amount(
+ current_amount, limit_consumed, certificate_limit, rate, tax_details
+):
+ if certificate_limit - flt(limit_consumed) - flt(current_amount) >= 0:
return current_amount * rate / 100
else:
- ltds_amount = certificate_limit - flt(deducted_amount)
+ ltds_amount = certificate_limit - flt(limit_consumed)
tds_amount = current_amount - ltds_amount
return ltds_amount * rate / 100 + tds_amount * tax_details.rate / 100
-def is_valid_certificate(
- valid_from, valid_upto, posting_date, deducted_amount, current_amount, certificate_limit
-):
- valid = False
+def is_valid_certificate(ldc, posting_date, limit_consumed):
+ available_amount = flt(ldc.certificate_limit) - flt(limit_consumed)
+ if (
+ getdate(ldc.valid_from) <= getdate(posting_date) <= getdate(ldc.valid_upto)
+ ) and available_amount > 0:
+ return True
- available_amount = flt(certificate_limit) - flt(deducted_amount)
-
- if (getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)) and available_amount > 0:
- valid = True
-
- return valid
+ return False
def normal_round(number):
diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
index 80220e4..0fbaf23 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
@@ -4,6 +4,7 @@
import unittest
import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.utils import today
from erpnext.accounts.utils import get_fiscal_year
@@ -17,6 +18,7 @@
# create relevant supplier, etc
create_records()
create_tax_withholding_category_records()
+ make_pan_no_field()
def tearDown(self):
cancel_invoices()
@@ -451,6 +453,40 @@
pe2.cancel()
pe3.cancel()
+ def test_lower_deduction_certificate_application(self):
+ frappe.db.set_value(
+ "Supplier",
+ "Test LDC Supplier",
+ {
+ "tax_withholding_category": "Test Service Category",
+ "pan": "ABCTY1234D",
+ },
+ )
+
+ create_lower_deduction_certificate(
+ supplier="Test LDC Supplier",
+ certificate_no="1AE0423AAJ",
+ tax_withholding_category="Test Service Category",
+ tax_rate=2,
+ limit=50000,
+ )
+
+ pi1 = create_purchase_invoice(supplier="Test LDC Supplier", rate=35000)
+ pi1.submit()
+ self.assertEqual(pi1.taxes[0].tax_amount, 700)
+
+ pi2 = create_purchase_invoice(supplier="Test LDC Supplier", rate=35000)
+ pi2.submit()
+ self.assertEqual(pi2.taxes[0].tax_amount, 2300)
+
+ pi3 = create_purchase_invoice(supplier="Test LDC Supplier", rate=35000)
+ pi3.submit()
+ self.assertEqual(pi3.taxes[0].tax_amount, 3500)
+
+ pi1.cancel()
+ pi2.cancel()
+ pi3.cancel()
+
def cancel_invoices():
purchase_invoices = frappe.get_all(
@@ -610,6 +646,7 @@
"Test TDS Supplier6",
"Test TDS Supplier7",
"Test TDS Supplier8",
+ "Test LDC Supplier",
]:
if frappe.db.exists("Supplier", name):
continue
@@ -806,3 +843,39 @@
"accounts": [{"company": "_Test Company", "account": account}],
}
).insert()
+
+
+def create_lower_deduction_certificate(
+ supplier, tax_withholding_category, tax_rate, certificate_no, limit
+):
+ fiscal_year = get_fiscal_year(today(), company="_Test Company")
+ if not frappe.db.exists("Lower Deduction Certificate", certificate_no):
+ frappe.get_doc(
+ {
+ "doctype": "Lower Deduction Certificate",
+ "company": "_Test Company",
+ "supplier": supplier,
+ "certificate_no": certificate_no,
+ "tax_withholding_category": tax_withholding_category,
+ "fiscal_year": fiscal_year[0],
+ "valid_from": fiscal_year[1],
+ "valid_upto": fiscal_year[2],
+ "rate": tax_rate,
+ "certificate_limit": limit,
+ }
+ ).insert()
+
+
+def make_pan_no_field():
+ pan_field = {
+ "Supplier": [
+ {
+ "fieldname": "pan",
+ "label": "PAN",
+ "fieldtype": "Data",
+ "translatable": 0,
+ }
+ ]
+ }
+
+ create_custom_fields(pan_field, update=1)
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index 895c314..8bd7b5a 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -14,7 +14,7 @@
from frappe.contacts.doctype.contact.contact import get_contact_details
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
from frappe.model.utils import get_fetch_values
-from frappe.query_builder.functions import Date, Sum
+from frappe.query_builder.functions import Abs, Date, Sum
from frappe.utils import (
add_days,
add_months,
@@ -707,6 +707,7 @@
if party_type not in ("Customer", "Supplier"):
return
template = None
+
if party_type == "Customer":
customer = frappe.get_cached_value(
"Customer", party_name, fieldname=["payment_terms", "customer_group"], as_dict=1
@@ -921,35 +922,34 @@
def get_partywise_advanced_payment_amount(
- party_type, posting_date=None, future_payment=0, company=None, party=None, account_type=None
+ party_type, posting_date=None, future_payment=0, company=None, party=None
):
- gle = frappe.qb.DocType("GL Entry")
+ ple = frappe.qb.DocType("Payment Ledger Entry")
query = (
- frappe.qb.from_(gle)
- .select(gle.party)
+ frappe.qb.from_(ple)
+ .select(ple.party, Abs(Sum(ple.amount).as_("amount")))
.where(
- (gle.party_type.isin(party_type)) & (gle.against_voucher.isnull()) & (gle.is_cancelled == 0)
+ (ple.party_type.isin(party_type))
+ & (ple.amount < 0)
+ & (ple.against_voucher_no == ple.voucher_no)
+ & (ple.delinked == 0)
)
- .groupby(gle.party)
+ .groupby(ple.party)
)
- if account_type == "Receivable":
- query = query.select(Sum(gle.credit).as_("amount"))
- else:
- query = query.select(Sum(gle.debit).as_("amount"))
if posting_date:
if future_payment:
- query = query.where((gle.posting_date <= posting_date) | (Date(gle.creation) <= posting_date))
+ query = query.where((ple.posting_date <= posting_date) | (Date(ple.creation) <= posting_date))
else:
- query = query.where(gle.posting_date <= posting_date)
+ query = query.where(ple.posting_date <= posting_date)
if company:
- query = query.where(gle.company == company)
+ query = query.where(ple.company == company)
if party:
- query = query.where(gle.party == party)
+ query = query.where(ple.party == party)
- data = query.run(as_dict=True)
+ data = query.run()
if data:
return frappe._dict(data)
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index 11bbb6f..751063a 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -214,8 +214,8 @@
for party_type in self.party_type:
if self.filters.get(scrub(party_type)):
amount = ple.amount_in_account_currency
- else:
- amount = ple.amount
+ else:
+ amount = ple.amount
amount_in_account_currency = ple.amount_in_account_currency
# update voucher
@@ -436,12 +436,11 @@
def allocate_outstanding_based_on_payment_terms(self, row):
self.get_payment_terms(row)
for term in row.payment_terms:
-
- # update "paid" and "oustanding" for this term
+ # update "paid" and "outstanding" for this term
if not term.paid:
self.allocate_closing_to_term(row, term, "paid")
- # update "credit_note" and "oustanding" for this term
+ # update "credit_note" and "outstanding" for this term
if term.outstanding:
self.allocate_closing_to_term(row, term, "credit_note")
@@ -453,7 +452,8 @@
"""
select
si.name, si.party_account_currency, si.currency, si.conversion_rate,
- ps.due_date, ps.payment_term, ps.payment_amount, ps.description, ps.paid_amount, ps.discounted_amount
+ si.total_advance, ps.due_date, ps.payment_term, ps.payment_amount, ps.base_payment_amount,
+ ps.description, ps.paid_amount, ps.discounted_amount
from `tab{0}` si, `tabPayment Schedule` ps
where
si.name = ps.parent and
@@ -469,6 +469,10 @@
original_row = frappe._dict(row)
row.payment_terms = []
+ # Advance allocated during invoicing is not considered in payment terms
+ # Deduct that from paid amount pre allocation
+ row.paid -= flt(payment_terms_details[0].total_advance)
+
# If no or single payment terms, no need to split the row
if len(payment_terms_details) <= 1:
return
@@ -483,7 +487,7 @@
) and d.currency == d.party_account_currency:
invoiced = d.payment_amount
else:
- invoiced = flt(flt(d.payment_amount) * flt(d.conversion_rate), self.currency_precision)
+ invoiced = d.base_payment_amount
row.payment_terms.append(
term.update(
@@ -1086,7 +1090,10 @@
.where(
(je.company == self.filters.company)
& (je.posting_date.lte(self.filters.report_date))
- & (je.voucher_type == "Exchange Rate Revaluation")
+ & (
+ (je.voucher_type == "Exchange Rate Revaluation")
+ | (je.voucher_type == "Exchange Gain Or Loss")
+ )
)
.run()
)
diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
index da4c9da..cffc878 100644
--- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
+++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
@@ -50,13 +50,12 @@
self.filters.show_future_payments,
self.filters.company,
party=party,
- account_type=self.account_type,
)
or {}
)
if self.filters.show_gl_balance:
- gl_balance_map = get_gl_balance(self.filters.report_date)
+ gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company)
for party, party_dict in self.party_total.items():
if party_dict.outstanding == 0:
@@ -233,12 +232,12 @@
self.add_column(label="Total Amount Due", fieldname="total_due")
-def get_gl_balance(report_date):
+def get_gl_balance(report_date, company):
return frappe._dict(
frappe.db.get_all(
"GL Entry",
fields=["party", "sum(debit - credit)"],
- filters={"posting_date": ("<=", report_date), "is_cancelled": 0},
+ filters={"posting_date": ("<=", report_date), "is_cancelled": 0, "company": company},
group_by="party",
as_list=1,
)
diff --git a/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py
new file mode 100644
index 0000000..3ee35a1
--- /dev/null
+++ b/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py
@@ -0,0 +1,203 @@
+import unittest
+
+import frappe
+from frappe.tests.utils import FrappeTestCase, change_settings
+from frappe.utils import today
+
+from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_summary import execute
+from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
+
+
+class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
+ def setUp(self):
+ self.maxDiff = None
+ self.create_company()
+ self.create_customer()
+ self.create_item()
+ self.clear_old_entries()
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+ def test_01_receivable_summary_output(self):
+ """
+ Test for Invoices, Paid, Advance and Outstanding
+ """
+ filters = {
+ "company": self.company,
+ "customer": self.customer,
+ "posting_date": today(),
+ "range1": 30,
+ "range2": 60,
+ "range3": 90,
+ "range4": 120,
+ }
+
+ si = create_sales_invoice(
+ item=self.item,
+ company=self.company,
+ customer=self.customer,
+ debit_to=self.debit_to,
+ posting_date=today(),
+ parent_cost_center=self.cost_center,
+ cost_center=self.cost_center,
+ rate=200,
+ price_list_rate=200,
+ )
+
+ customer_group, customer_territory = frappe.db.get_all(
+ "Customer",
+ filters={"name": self.customer},
+ fields=["customer_group", "territory"],
+ as_list=True,
+ )[0]
+
+ report = execute(filters)
+ rpt_output = report[1]
+ expected_data = {
+ "party_type": "Customer",
+ "advance": 0,
+ "party": self.customer,
+ "invoiced": 200.0,
+ "paid": 0.0,
+ "credit_note": 0.0,
+ "outstanding": 200.0,
+ "range1": 200.0,
+ "range2": 0.0,
+ "range3": 0.0,
+ "range4": 0.0,
+ "range5": 0.0,
+ "total_due": 200.0,
+ "future_amount": 0.0,
+ "sales_person": [],
+ "currency": si.currency,
+ "territory": customer_territory,
+ "customer_group": customer_group,
+ }
+
+ self.assertEqual(len(rpt_output), 1)
+ self.assertDictEqual(rpt_output[0], expected_data)
+
+ # simulate advance payment
+ pe = get_payment_entry(si.doctype, si.name)
+ pe.paid_amount = 50
+ pe.references[0].allocated_amount = 0 # this essitially removes the reference
+ pe.save().submit()
+
+ # update expected data with advance
+ expected_data.update(
+ {
+ "advance": 50.0,
+ "outstanding": 150.0,
+ "range1": 150.0,
+ "total_due": 150.0,
+ }
+ )
+
+ report = execute(filters)
+ rpt_output = report[1]
+ self.assertEqual(len(rpt_output), 1)
+ self.assertDictEqual(rpt_output[0], expected_data)
+
+ # make partial payment
+ pe = get_payment_entry(si.doctype, si.name)
+ pe.paid_amount = 125
+ pe.references[0].allocated_amount = 125
+ pe.save().submit()
+
+ # update expected data after advance and partial payment
+ expected_data.update(
+ {"advance": 50.0, "paid": 125.0, "outstanding": 25.0, "range1": 25.0, "total_due": 25.0}
+ )
+
+ report = execute(filters)
+ rpt_output = report[1]
+ self.assertEqual(len(rpt_output), 1)
+ self.assertDictEqual(rpt_output[0], expected_data)
+
+ @change_settings("Selling Settings", {"cust_master_name": "Naming Series"})
+ def test_02_various_filters_and_output(self):
+ filters = {
+ "company": self.company,
+ "customer": self.customer,
+ "posting_date": today(),
+ "range1": 30,
+ "range2": 60,
+ "range3": 90,
+ "range4": 120,
+ }
+
+ si = create_sales_invoice(
+ item=self.item,
+ company=self.company,
+ customer=self.customer,
+ debit_to=self.debit_to,
+ posting_date=today(),
+ parent_cost_center=self.cost_center,
+ cost_center=self.cost_center,
+ rate=200,
+ price_list_rate=200,
+ )
+ # make partial payment
+ pe = get_payment_entry(si.doctype, si.name)
+ pe.paid_amount = 150
+ pe.references[0].allocated_amount = 150
+ pe.save().submit()
+
+ customer_group, customer_territory = frappe.db.get_all(
+ "Customer",
+ filters={"name": self.customer},
+ fields=["customer_group", "territory"],
+ as_list=True,
+ )[0]
+
+ report = execute(filters)
+ rpt_output = report[1]
+ expected_data = {
+ "party_type": "Customer",
+ "advance": 0,
+ "party": self.customer,
+ "party_name": self.customer,
+ "invoiced": 200.0,
+ "paid": 150.0,
+ "credit_note": 0.0,
+ "outstanding": 50.0,
+ "range1": 50.0,
+ "range2": 0.0,
+ "range3": 0.0,
+ "range4": 0.0,
+ "range5": 0.0,
+ "total_due": 50.0,
+ "future_amount": 0.0,
+ "sales_person": [],
+ "currency": si.currency,
+ "territory": customer_territory,
+ "customer_group": customer_group,
+ }
+
+ self.assertEqual(len(rpt_output), 1)
+ self.assertDictEqual(rpt_output[0], expected_data)
+
+ # with gl balance filter
+ filters.update({"show_gl_balance": True})
+ expected_data.update({"gl_balance": 50.0, "diff": 0.0})
+ report = execute(filters)
+ rpt_output = report[1]
+ self.assertEqual(len(rpt_output), 1)
+ self.assertDictEqual(rpt_output[0], expected_data)
+
+ # with gl balance and future payments filter
+ filters.update({"show_future_payments": True})
+ expected_data.update({"remaining_balance": 50.0})
+ report = execute(filters)
+ rpt_output = report[1]
+ self.assertEqual(len(rpt_output), 1)
+ self.assertDictEqual(rpt_output[0], expected_data)
+
+ # invoice fully paid
+ pe = get_payment_entry(si.doctype, si.name).save().submit()
+ report = execute(filters)
+ rpt_output = report[1]
+ self.assertEqual(len(rpt_output), 0)
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
index 080e45a..0051ba6 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -744,13 +744,18 @@
if from_date:
additional_conditions.append(gle.posting_date >= from_date)
- finance_book = filters.get("finance_book")
- company_fb = frappe.get_cached_value("Company", d.name, "default_finance_book")
+ finance_books = []
+ finance_books.append("")
+ if filter_fb := filters.get("finance_book"):
+ finance_books.append(filter_fb)
if filters.get("include_default_book_entries"):
- additional_conditions.append((gle.finance_book.isin([finance_book, company_fb, "", None])))
+ if company_fb := frappe.get_cached_value("Company", d.name, "default_finance_book"):
+ finance_books.append(company_fb)
+
+ additional_conditions.append((gle.finance_book.isin(finance_books)) | gle.finance_book.isnull())
else:
- additional_conditions.append((gle.finance_book.isin([finance_book, "", None])))
+ additional_conditions.append((gle.finance_book.isin(finance_books)) | gle.finance_book.isnull())
return additional_conditions
diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py
index a76dea6..693725d 100644
--- a/erpnext/accounts/report/financial_statements.py
+++ b/erpnext/accounts/report/financial_statements.py
@@ -335,12 +335,10 @@
for period in period_list:
total_row.setdefault(period.key, 0.0)
total_row[period.key] += row.get(period.key, 0.0)
- row[period.key] = row.get(period.key, 0.0)
total_row.setdefault("total", 0.0)
total_row["total"] += flt(row["total"])
total_row["opening_balance"] += row["opening_balance"]
- row["total"] = ""
if "total" in total_row:
out.append(total_row)
@@ -639,7 +637,13 @@
if periodicity != "Yearly":
if not accumulated_values:
columns.append(
- {"fieldname": "total", "label": _("Total"), "fieldtype": "Currency", "width": 150}
+ {
+ "fieldname": "total",
+ "label": _("Total"),
+ "fieldtype": "Currency",
+ "width": 150,
+ "options": "currency",
+ }
)
return columns
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js
index 57a9091..37d0659 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.js
+++ b/erpnext/accounts/report/general_ledger/general_ledger.js
@@ -188,6 +188,11 @@
"fieldname": "show_net_values_in_party_account",
"label": __("Show Net Values in Party Account"),
"fieldtype": "Check"
+ },
+ {
+ "fieldname": "add_values_in_transaction_currency",
+ "label": __("Add Columns in Transaction Currency"),
+ "fieldtype": "Check"
}
]
}
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index d7af167..e05a4e7 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -182,12 +182,18 @@
if accounting_dimensions:
dimension_fields = ", ".join(accounting_dimensions) + ","
+ transaction_currency_fields = ""
+ if filters.get("add_values_in_transaction_currency"):
+ transaction_currency_fields = (
+ "debit_in_transaction_currency, credit_in_transaction_currency, transaction_currency,"
+ )
+
gl_entries = frappe.db.sql(
"""
select
name as gl_entry, posting_date, account, party_type, party,
voucher_type, voucher_no, {dimension_fields}
- cost_center, project,
+ cost_center, project, {transaction_currency_fields}
against_voucher_type, against_voucher, account_currency,
remarks, against, is_opening, creation {select_fields}
from `tabGL Entry`
@@ -195,6 +201,7 @@
{order_by_statement}
""".format(
dimension_fields=dimension_fields,
+ transaction_currency_fields=transaction_currency_fields,
select_fields=select_fields,
conditions=get_conditions(filters),
order_by_statement=order_by_statement,
@@ -562,6 +569,34 @@
"fieldtype": "Float",
"width": 130,
},
+ ]
+
+ if filters.get("add_values_in_transaction_currency"):
+ columns += [
+ {
+ "label": _("Debit (Transaction)"),
+ "fieldname": "debit_in_transaction_currency",
+ "fieldtype": "Currency",
+ "width": 130,
+ "options": "transaction_currency",
+ },
+ {
+ "label": _("Credit (Transaction)"),
+ "fieldname": "credit_in_transaction_currency",
+ "fieldtype": "Currency",
+ "width": 130,
+ "options": "transaction_currency",
+ },
+ {
+ "label": "Transaction Currency",
+ "fieldname": "transaction_currency",
+ "fieldtype": "Link",
+ "options": "Currency",
+ "width": 70,
+ },
+ ]
+
+ columns += [
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 120},
{
"label": _("Voucher No"),
diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.js b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.js
index b66a555..8808165 100644
--- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.js
+++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.js
@@ -33,7 +33,14 @@
frappe.throw(__("Please select Party Type first"));
}
return party_type;
- }
+ },
+ "get_query": function() {
+ return {
+ "filters": {
+ "tax_withholding_category": ["!=",""],
+ }
+ }
+ },
},
{
"fieldname":"from_date",
diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py
index ddd049a..7d16661 100644
--- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py
+++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py
@@ -7,19 +7,26 @@
def execute(filters=None):
+ if filters.get("party_type") == "Customer":
+ party_naming_by = frappe.db.get_single_value("Selling Settings", "cust_master_name")
+ else:
+ party_naming_by = frappe.db.get_single_value("Buying Settings", "supp_master_name")
+
+ filters.update({"naming_series": party_naming_by})
+
validate_filters(filters)
(
tds_docs,
tds_accounts,
tax_category_map,
journal_entry_party_map,
- invoice_net_total_map,
+ net_total_map,
) = get_tds_docs(filters)
columns = get_columns(filters)
res = get_result(
- filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, invoice_net_total_map
+ filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, net_total_map
)
return columns, res
@@ -31,7 +38,7 @@
def get_result(
- filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, invoice_net_total_map
+ filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, net_total_map
):
party_map = get_party_pan_map(filters.get("party_type"))
tax_rate_map = get_tax_rate_map(filters)
@@ -39,7 +46,7 @@
out = []
for name, details in gle_map.items():
- tax_amount, total_amount = 0, 0
+ tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0
tax_withholding_category = tax_category_map.get(name)
rate = tax_rate_map.get(tax_withholding_category)
@@ -60,8 +67,8 @@
if entry.account in tds_accounts:
tax_amount += entry.credit - entry.debit
- if invoice_net_total_map.get(name):
- total_amount = invoice_net_total_map.get(name)
+ if net_total_map.get(name):
+ total_amount, grand_total, base_total = net_total_map.get(name)
else:
total_amount += entry.credit
@@ -69,15 +76,13 @@
if party_map.get(party, {}).get("party_type") == "Supplier":
party_name = "supplier_name"
party_type = "supplier_type"
- table_name = "Supplier"
else:
party_name = "customer_name"
party_type = "customer_type"
- table_name = "Customer"
row = {
"pan"
- if frappe.db.has_column(table_name, "pan")
+ if frappe.db.has_column(filters.party_type, "pan")
else "tax_id": party_map.get(party, {}).get("pan"),
"party": party_map.get(party, {}).get("name"),
}
@@ -91,6 +96,8 @@
"entity_type": party_map.get(party, {}).get(party_type),
"rate": rate,
"total_amount": total_amount,
+ "grand_total": grand_total,
+ "base_total": base_total,
"tax_amount": tax_amount,
"transaction_date": posting_date,
"transaction_type": voucher_type,
@@ -144,9 +151,9 @@
def get_columns(filters):
- pan = "pan" if frappe.db.has_column("Supplier", "pan") else "tax_id"
+ pan = "pan" if frappe.db.has_column(filters.party_type, "pan") else "tax_id"
columns = [
- {"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 90},
+ {"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 60},
{
"label": _(filters.get("party_type")),
"fieldname": "party",
@@ -158,25 +165,30 @@
if filters.naming_series == "Naming Series":
columns.append(
- {"label": _("Party Name"), "fieldname": "party_name", "fieldtype": "Data", "width": 180}
+ {
+ "label": _(filters.party_type + " Name"),
+ "fieldname": "party_name",
+ "fieldtype": "Data",
+ "width": 180,
+ }
)
columns.extend(
[
{
+ "label": _("Date of Transaction"),
+ "fieldname": "transaction_date",
+ "fieldtype": "Date",
+ "width": 100,
+ },
+ {
"label": _("Section Code"),
"options": "Tax Withholding Category",
"fieldname": "section_code",
"fieldtype": "Link",
- "width": 180,
- },
- {"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 120},
- {
- "label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"),
- "fieldname": "rate",
- "fieldtype": "Percent",
"width": 90,
},
+ {"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 100},
{
"label": _("Total Amount"),
"fieldname": "total_amount",
@@ -184,15 +196,27 @@
"width": 90,
},
{
- "label": _("TDS Amount") if filters.get("party_type") == "Supplier" else _("TCS Amount"),
+ "label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"),
+ "fieldname": "rate",
+ "fieldtype": "Percent",
+ "width": 90,
+ },
+ {
+ "label": _("Tax Amount"),
"fieldname": "tax_amount",
"fieldtype": "Float",
"width": 90,
},
{
- "label": _("Date of Transaction"),
- "fieldname": "transaction_date",
- "fieldtype": "Date",
+ "label": _("Grand Total"),
+ "fieldname": "grand_total",
+ "fieldtype": "Float",
+ "width": 90,
+ },
+ {
+ "label": _("Base Total"),
+ "fieldname": "base_total",
+ "fieldtype": "Float",
"width": 90,
},
{"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 100},
@@ -216,7 +240,7 @@
payment_entries = []
journal_entries = []
tax_category_map = frappe._dict()
- invoice_net_total_map = frappe._dict()
+ net_total_map = frappe._dict()
or_filters = frappe._dict()
journal_entry_party_map = frappe._dict()
bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name")
@@ -260,13 +284,13 @@
tds_documents.append(d.voucher_no)
if purchase_invoices:
- get_doc_info(purchase_invoices, "Purchase Invoice", tax_category_map, invoice_net_total_map)
+ get_doc_info(purchase_invoices, "Purchase Invoice", tax_category_map, net_total_map)
if sales_invoices:
- get_doc_info(sales_invoices, "Sales Invoice", tax_category_map, invoice_net_total_map)
+ get_doc_info(sales_invoices, "Sales Invoice", tax_category_map, net_total_map)
if payment_entries:
- get_doc_info(payment_entries, "Payment Entry", tax_category_map)
+ get_doc_info(payment_entries, "Payment Entry", tax_category_map, net_total_map)
if journal_entries:
journal_entry_party_map = get_journal_entry_party_map(journal_entries)
@@ -277,7 +301,7 @@
tds_accounts,
tax_category_map,
journal_entry_party_map,
- invoice_net_total_map,
+ net_total_map,
)
@@ -295,11 +319,25 @@
return journal_entry_party_map
-def get_doc_info(vouchers, doctype, tax_category_map, invoice_net_total_map=None):
+def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
if doctype == "Purchase Invoice":
- fields = ["name", "tax_withholding_category", "base_tax_withholding_net_total"]
- if doctype == "Sales Invoice":
- fields = ["name", "base_net_total"]
+ fields = [
+ "name",
+ "tax_withholding_category",
+ "base_tax_withholding_net_total",
+ "grand_total",
+ "base_total",
+ ]
+ elif doctype == "Sales Invoice":
+ fields = ["name", "base_net_total", "grand_total", "base_total"]
+ elif doctype == "Payment Entry":
+ fields = [
+ "name",
+ "tax_withholding_category",
+ "paid_amount",
+ "paid_amount_after_tax",
+ "base_paid_amount",
+ ]
else:
fields = ["name", "tax_withholding_category"]
@@ -308,9 +346,15 @@
for entry in entries:
tax_category_map.update({entry.name: entry.tax_withholding_category})
if doctype == "Purchase Invoice":
- invoice_net_total_map.update({entry.name: entry.base_tax_withholding_net_total})
- if doctype == "Sales Invoice":
- invoice_net_total_map.update({entry.name: entry.base_net_total})
+ net_total_map.update(
+ {entry.name: [entry.base_tax_withholding_net_total, entry.grand_total, entry.base_total]}
+ )
+ elif doctype == "Sales Invoice":
+ net_total_map.update({entry.name: [entry.base_net_total, entry.grand_total, entry.base_total]})
+ elif doctype == "Payment Entry":
+ net_total_map.update(
+ {entry.name: [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount]}
+ )
def get_tax_rate_map(filters):
diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.js b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.js
index d334846..a0be1b5 100644
--- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.js
+++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.js
@@ -12,17 +12,35 @@
"default": frappe.defaults.get_default('company')
},
{
- "fieldname":"supplier",
- "label": __("Supplier"),
- "fieldtype": "Link",
- "options": "Supplier",
+ "fieldname":"party_type",
+ "label": __("Party Type"),
+ "fieldtype": "Select",
+ "options": ["Supplier", "Customer"],
+ "reqd": 1,
+ "default": "Supplier",
+ "on_change": function(){
+ frappe.query_report.set_filter_value("party", "");
+ }
+ },
+ {
+ "fieldname":"party",
+ "label": __("Party"),
+ "fieldtype": "Dynamic Link",
+ "get_options": function() {
+ var party_type = frappe.query_report.get_filter_value('party_type');
+ var party = frappe.query_report.get_filter_value('party');
+ if(party && !party_type) {
+ frappe.throw(__("Please select Party Type first"));
+ }
+ return party_type;
+ },
"get_query": function() {
return {
"filters": {
"tax_withholding_category": ["!=",""],
}
}
- }
+ },
},
{
"fieldname":"from_date",
diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
index c6aa21c..82f97f1 100644
--- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
+++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
@@ -9,9 +9,14 @@
def execute(filters=None):
- validate_filters(filters)
+ if filters.get("party_type") == "Customer":
+ party_naming_by = frappe.db.get_single_value("Selling Settings", "cust_master_name")
+ else:
+ party_naming_by = frappe.db.get_single_value("Buying Settings", "supp_master_name")
- filters.naming_series = frappe.db.get_single_value("Buying Settings", "supp_master_name")
+ filters.update({"naming_series": party_naming_by})
+
+ validate_filters(filters)
columns = get_columns(filters)
(
@@ -25,7 +30,7 @@
res = get_result(
filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, invoice_total_map
)
- final_result = group_by_supplier_and_category(res)
+ final_result = group_by_party_and_category(res, filters)
return columns, final_result
@@ -43,60 +48,67 @@
filters["fiscal_year"] = from_year
-def group_by_supplier_and_category(data):
- supplier_category_wise_map = {}
+def group_by_party_and_category(data, filters):
+ party_category_wise_map = {}
for row in data:
- supplier_category_wise_map.setdefault(
- (row.get("supplier"), row.get("section_code")),
+ party_category_wise_map.setdefault(
+ (row.get("party"), row.get("section_code")),
{
"pan": row.get("pan"),
- "supplier": row.get("supplier"),
- "supplier_name": row.get("supplier_name"),
+ "tax_id": row.get("tax_id"),
+ "party": row.get("party"),
+ "party_name": row.get("party_name"),
"section_code": row.get("section_code"),
"entity_type": row.get("entity_type"),
- "tds_rate": row.get("tds_rate"),
- "total_amount_credited": 0.0,
- "tds_deducted": 0.0,
+ "rate": row.get("rate"),
+ "total_amount": 0.0,
+ "tax_amount": 0.0,
},
)
- supplier_category_wise_map.get((row.get("supplier"), row.get("section_code")))[
- "total_amount_credited"
- ] += row.get("total_amount_credited", 0.0)
+ party_category_wise_map.get((row.get("party"), row.get("section_code")))[
+ "total_amount"
+ ] += row.get("total_amount", 0.0)
- supplier_category_wise_map.get((row.get("supplier"), row.get("section_code")))[
- "tds_deducted"
- ] += row.get("tds_deducted", 0.0)
+ party_category_wise_map.get((row.get("party"), row.get("section_code")))[
+ "tax_amount"
+ ] += row.get("tax_amount", 0.0)
- final_result = get_final_result(supplier_category_wise_map)
+ final_result = get_final_result(party_category_wise_map)
return final_result
-def get_final_result(supplier_category_wise_map):
+def get_final_result(party_category_wise_map):
out = []
- for key, value in supplier_category_wise_map.items():
+ for key, value in party_category_wise_map.items():
out.append(value)
return out
def get_columns(filters):
+ pan = "pan" if frappe.db.has_column(filters.party_type, "pan") else "tax_id"
columns = [
- {"label": _("PAN"), "fieldname": "pan", "fieldtype": "Data", "width": 90},
+ {"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 90},
{
- "label": _("Supplier"),
- "options": "Supplier",
- "fieldname": "supplier",
- "fieldtype": "Link",
+ "label": _(filters.get("party_type")),
+ "fieldname": "party",
+ "fieldtype": "Dynamic Link",
+ "options": "party_type",
"width": 180,
},
]
if filters.naming_series == "Naming Series":
columns.append(
- {"label": _("Supplier Name"), "fieldname": "supplier_name", "fieldtype": "Data", "width": 180}
+ {
+ "label": _(filters.party_type + " Name"),
+ "fieldname": "party_name",
+ "fieldtype": "Data",
+ "width": 180,
+ }
)
columns.extend(
@@ -109,18 +121,23 @@
"width": 180,
},
{"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 180},
- {"label": _("TDS Rate %"), "fieldname": "tds_rate", "fieldtype": "Percent", "width": 90},
{
- "label": _("Total Amount Credited"),
- "fieldname": "total_amount_credited",
- "fieldtype": "Float",
- "width": 90,
+ "label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"),
+ "fieldname": "rate",
+ "fieldtype": "Percent",
+ "width": 120,
},
{
- "label": _("Amount of TDS Deducted"),
- "fieldname": "tds_deducted",
+ "label": _("Total Amount"),
+ "fieldname": "total_amount",
"fieldtype": "Float",
- "width": 90,
+ "width": 120,
+ },
+ {
+ "label": _("Tax Amount"),
+ "fieldname": "tax_amount",
+ "fieldtype": "Float",
+ "width": 120,
},
]
)
diff --git a/erpnext/accounts/test/accounts_mixin.py b/erpnext/accounts/test/accounts_mixin.py
index 70bbf7e..debfffd 100644
--- a/erpnext/accounts/test/accounts_mixin.py
+++ b/erpnext/accounts/test/accounts_mixin.py
@@ -1,4 +1,5 @@
import frappe
+from frappe import qb
from erpnext.stock.doctype.item.test_item import create_item
@@ -103,3 +104,15 @@
)
new_acc.save()
setattr(self, acc.attribute_name, new_acc.name)
+
+ def clear_old_entries(self):
+ doctype_list = [
+ "GL Entry",
+ "Payment Ledger Entry",
+ "Sales Invoice",
+ "Purchase Invoice",
+ "Payment Entry",
+ "Journal Entry",
+ ]
+ for doctype in doctype_list:
+ qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index c24442e..1aefeaa 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -908,6 +908,9 @@
min_outstanding=None,
max_outstanding=None,
accounting_dimensions=None,
+ vouchers=None, # list of dicts [{'voucher_type': '', 'voucher_no': ''}] for filtering
+ limit=None, # passed by reconciliation tool
+ voucher_no=None, # filter passed by reconciliation tool
):
ple = qb.DocType("Payment Ledger Entry")
@@ -933,12 +936,15 @@
ple_query = QueryPaymentLedger()
invoice_list = ple_query.get_voucher_outstandings(
+ vouchers=vouchers,
common_filter=common_filter,
posting_date=posting_date,
min_outstanding=min_outstanding,
max_outstanding=max_outstanding,
get_invoices=True,
accounting_dimensions=accounting_dimensions or [],
+ limit=limit,
+ voucher_no=voucher_no,
)
for d in invoice_list:
@@ -1676,12 +1682,13 @@
self.voucher_posting_date = []
self.min_outstanding = None
self.max_outstanding = None
+ self.limit = self.voucher_no = None
def reset(self):
# clear filters
self.vouchers.clear()
self.common_filter.clear()
- self.min_outstanding = self.max_outstanding = None
+ self.min_outstanding = self.max_outstanding = self.limit = None
# clear result
self.voucher_outstandings.clear()
@@ -1695,6 +1702,7 @@
filter_on_voucher_no = []
filter_on_against_voucher_no = []
+
if self.vouchers:
voucher_types = set([x.voucher_type for x in self.vouchers])
voucher_nos = set([x.voucher_no for x in self.vouchers])
@@ -1705,6 +1713,10 @@
filter_on_against_voucher_no.append(ple.against_voucher_type.isin(voucher_types))
filter_on_against_voucher_no.append(ple.against_voucher_no.isin(voucher_nos))
+ if self.voucher_no:
+ filter_on_voucher_no.append(ple.voucher_no.like(f"%{self.voucher_no}%"))
+ filter_on_against_voucher_no.append(ple.against_voucher_no.like(f"%{self.voucher_no}%"))
+
# build outstanding amount filter
filter_on_outstanding_amount = []
if self.min_outstanding:
@@ -1820,6 +1832,11 @@
)
)
+ if self.limit:
+ self.cte_query_voucher_amount_and_outstanding = (
+ self.cte_query_voucher_amount_and_outstanding.limit(self.limit)
+ )
+
# execute SQL
self.voucher_outstandings = self.cte_query_voucher_amount_and_outstanding.run(as_dict=True)
@@ -1833,6 +1850,8 @@
get_payments=False,
get_invoices=False,
accounting_dimensions=None,
+ limit=None,
+ voucher_no=None,
):
"""
Fetch voucher amount and outstanding amount from Payment Ledger using Database CTE
@@ -1854,6 +1873,8 @@
self.max_outstanding = max_outstanding
self.get_payments = get_payments
self.get_invoices = get_invoices
+ self.limit = limit
+ self.voucher_no = voucher_no
self.query_for_outstanding()
return self.voucher_outstandings
diff --git a/erpnext/accounts/workspace/accounting/accounting.json b/erpnext/accounts/workspace/accounting/accounting.json
index c27ede2..dfdae1d 100644
--- a/erpnext/accounts/workspace/accounting/accounting.json
+++ b/erpnext/accounts/workspace/accounting/accounting.json
@@ -5,7 +5,7 @@
"label": "Profit and Loss"
}
],
- "content": "[{\"id\":\"MmUf9abwxg\",\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Accounts\",\"col\":12}},{\"id\":\"i0EtSjDAXq\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Profit and Loss\",\"col\":12}},{\"id\":\"X78jcbq1u3\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"vikWSkNm6_\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"pMywM0nhlj\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Chart of Accounts\",\"col\":3}},{\"id\":\"_pRdD6kqUG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"id\":\"G984SgVRJN\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Invoice\",\"col\":3}},{\"id\":\"1ArNvt9qhz\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Journal Entry\",\"col\":3}},{\"id\":\"F9f4I1viNr\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Payment Entry\",\"col\":3}},{\"id\":\"4IBBOIxfqW\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Receivable\",\"col\":3}},{\"id\":\"El2anpPaFY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"General Ledger\",\"col\":3}},{\"id\":\"1nwcM9upJo\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Trial Balance\",\"col\":3}},{\"id\":\"OF9WOi1Ppc\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"id\":\"iAwpe-Chra\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Accounting\",\"col\":3}},{\"id\":\"B7-uxs8tkU\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"tHb3yxthkR\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"id\":\"DnNtsmxpty\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounting Masters\",\"col\":4}},{\"id\":\"nKKr6fjgjb\",\"type\":\"card\",\"data\":{\"card_name\":\"General Ledger\",\"col\":4}},{\"id\":\"xOHTyD8b5l\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Receivable\",\"col\":4}},{\"id\":\"_Cb7C8XdJJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Payable\",\"col\":4}},{\"id\":\"p7NY6MHe2Y\",\"type\":\"card\",\"data\":{\"card_name\":\"Financial Statements\",\"col\":4}},{\"id\":\"KlqilF5R_V\",\"type\":\"card\",\"data\":{\"card_name\":\"Taxes\",\"col\":4}},{\"id\":\"jTUy8LB0uw\",\"type\":\"card\",\"data\":{\"card_name\":\"Cost Center and Budgeting\",\"col\":4}},{\"id\":\"Wn2lhs7WLn\",\"type\":\"card\",\"data\":{\"card_name\":\"Multi Currency\",\"col\":4}},{\"id\":\"PAQMqqNkBM\",\"type\":\"card\",\"data\":{\"card_name\":\"Bank Statement\",\"col\":4}},{\"id\":\"Q_hBCnSeJY\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"3AK1Zf0oew\",\"type\":\"card\",\"data\":{\"card_name\":\"Profitability\",\"col\":4}},{\"id\":\"kxhoaiqdLq\",\"type\":\"card\",\"data\":{\"card_name\":\"Opening and Closing\",\"col\":4}},{\"id\":\"q0MAlU2j_Z\",\"type\":\"card\",\"data\":{\"card_name\":\"Subscription Management\",\"col\":4}},{\"id\":\"ptm7T6Hwu-\",\"type\":\"card\",\"data\":{\"card_name\":\"Share Management\",\"col\":4}},{\"id\":\"OX7lZHbiTr\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
+ "content": "[{\"id\":\"MmUf9abwxg\",\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Accounts\",\"col\":12}},{\"id\":\"VVvJ1lUcfc\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Outgoing Bills\",\"col\":3}},{\"id\":\"Vlj2FZtlHV\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Incoming Bills\",\"col\":3}},{\"id\":\"VVVjQVAhPf\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Incoming Payment\",\"col\":3}},{\"id\":\"DySNdlysIW\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Outgoing Payment\",\"col\":3}},{\"id\":\"i0EtSjDAXq\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Profit and Loss\",\"col\":12}},{\"id\":\"X78jcbq1u3\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"vikWSkNm6_\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"pMywM0nhlj\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Chart of Accounts\",\"col\":3}},{\"id\":\"_pRdD6kqUG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"id\":\"G984SgVRJN\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Invoice\",\"col\":3}},{\"id\":\"1ArNvt9qhz\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Journal Entry\",\"col\":3}},{\"id\":\"F9f4I1viNr\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Payment Entry\",\"col\":3}},{\"id\":\"4IBBOIxfqW\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Receivable\",\"col\":3}},{\"id\":\"El2anpPaFY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"General Ledger\",\"col\":3}},{\"id\":\"1nwcM9upJo\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Trial Balance\",\"col\":3}},{\"id\":\"OF9WOi1Ppc\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"id\":\"iAwpe-Chra\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Accounting\",\"col\":3}},{\"id\":\"B7-uxs8tkU\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"tHb3yxthkR\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"id\":\"DnNtsmxpty\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounting Masters\",\"col\":4}},{\"id\":\"nKKr6fjgjb\",\"type\":\"card\",\"data\":{\"card_name\":\"General Ledger\",\"col\":4}},{\"id\":\"xOHTyD8b5l\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Receivable\",\"col\":4}},{\"id\":\"_Cb7C8XdJJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Payable\",\"col\":4}},{\"id\":\"p7NY6MHe2Y\",\"type\":\"card\",\"data\":{\"card_name\":\"Financial Statements\",\"col\":4}},{\"id\":\"KlqilF5R_V\",\"type\":\"card\",\"data\":{\"card_name\":\"Taxes\",\"col\":4}},{\"id\":\"jTUy8LB0uw\",\"type\":\"card\",\"data\":{\"card_name\":\"Cost Center and Budgeting\",\"col\":4}},{\"id\":\"Wn2lhs7WLn\",\"type\":\"card\",\"data\":{\"card_name\":\"Multi Currency\",\"col\":4}},{\"id\":\"PAQMqqNkBM\",\"type\":\"card\",\"data\":{\"card_name\":\"Bank Statement\",\"col\":4}},{\"id\":\"Q_hBCnSeJY\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"3AK1Zf0oew\",\"type\":\"card\",\"data\":{\"card_name\":\"Profitability\",\"col\":4}},{\"id\":\"kxhoaiqdLq\",\"type\":\"card\",\"data\":{\"card_name\":\"Opening and Closing\",\"col\":4}},{\"id\":\"q0MAlU2j_Z\",\"type\":\"card\",\"data\":{\"card_name\":\"Subscription Management\",\"col\":4}},{\"id\":\"ptm7T6Hwu-\",\"type\":\"card\",\"data\":{\"card_name\":\"Share Management\",\"col\":4}},{\"id\":\"OX7lZHbiTr\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
"creation": "2020-03-02 15:41:59.515192",
"custom_blocks": [],
"docstatus": 0,
@@ -1061,11 +1061,28 @@
"type": "Link"
}
],
- "modified": "2023-07-04 14:32:15.842044",
+ "modified": "2023-08-10 17:41:14.059005",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting",
- "number_cards": [],
+ "number_cards": [
+ {
+ "label": "Total Outgoing Bills",
+ "number_card_name": "Total Outgoing Bills"
+ },
+ {
+ "label": "Total Incoming Bills",
+ "number_card_name": "Total Incoming Bills"
+ },
+ {
+ "label": "Total Incoming Payment",
+ "number_card_name": "Total Incoming Payment"
+ },
+ {
+ "label": "Total Outgoing Payment",
+ "number_card_name": "Total Outgoing Payment"
+ }
+ ],
"owner": "Administrator",
"parent_page": "",
"public": 1,
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index 0a2f61d..962292b 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -228,15 +228,19 @@
{name: __("Schedule Date"), editable: false, resizable: false, width: 270},
{name: __("Depreciation Amount"), editable: false, resizable: false, width: 164},
{name: __("Accumulated Depreciation Amount"), editable: false, resizable: false, width: 164},
- {name: __("Journal Entry"), editable: false, resizable: false, format: value => `<a href="/app/journal-entry/${value}">${value}</a>`, width: 312}
+ {name: __("Journal Entry"), editable: false, resizable: false, format: value => `<a href="/app/journal-entry/${value}">${value}</a>`, width: 304}
],
data: data,
+ layout: "fluid",
serialNoColumn: false,
checkboxColumn: true,
cellHeight: 35
});
- datatable.style.setStyle(`.dt-scrollable`, {'font-size': '0.75rem', 'margin-bottom': '1rem'});
+ datatable.style.setStyle(`.dt-scrollable`, {'font-size': '0.75rem', 'margin-bottom': '1rem', 'margin-left': '0.35rem', 'margin-right': '0.35rem'});
+ datatable.style.setStyle(`.dt-header`, {'margin-left': '0.35rem', 'margin-right': '0.35rem'});
+ datatable.style.setStyle(`.dt-cell--header`, {'color': 'var(--text-muted)'});
+ datatable.style.setStyle(`.dt-cell`, {'color': 'var(--text-color)'});
datatable.style.setStyle(`.dt-cell--col-1`, {'text-align': 'center'});
datatable.style.setStyle(`.dt-cell--col-2`, {'font-weight': 600});
datatable.style.setStyle(`.dt-cell--col-3`, {'font-weight': 600});
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 2060c6c..ddb09c1 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -56,7 +56,6 @@
def on_submit(self):
self.validate_in_use_date()
- self.set_status()
self.make_asset_movement()
if not self.booked_fixed_asset and self.validate_make_gl_entry():
self.make_gl_entries()
@@ -72,6 +71,7 @@
"Asset Depreciation Schedules created:<br>{0}<br><br>Please check, edit if needed, and submit the Asset."
).format(asset_depr_schedules_links)
)
+ self.set_status()
add_asset_activity(self.name, _("Asset submitted"))
def on_cancel(self):
@@ -96,11 +96,14 @@
"Asset Depreciation Schedules created:<br>{0}<br><br>Please check, edit if needed, and submit the Asset."
).format(asset_depr_schedules_links)
)
- if not frappe.db.exists(
- {
- "doctype": "Asset Activity",
- "asset": self.name,
- }
+ if (
+ not frappe.db.exists(
+ {
+ "doctype": "Asset Activity",
+ "asset": self.name,
+ }
+ )
+ and not self.flags.asset_created_via_asset_capitalization
):
add_asset_activity(self.name, _("Asset created"))
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index cd66f1d..90eae2d 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -19,6 +19,7 @@
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.assets.doctype.asset.asset import (
+ get_asset_value_after_depreciation,
make_sales_invoice,
split_asset,
update_maintenance_status,
diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
index 324b739..0bf2fbb 100644
--- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
+++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
@@ -509,6 +509,7 @@
asset_doc.gross_purchase_amount = total_target_asset_value
asset_doc.purchase_receipt_amount = total_target_asset_value
asset_doc.flags.ignore_validate = True
+ asset_doc.flags.asset_created_via_asset_capitalization = True
asset_doc.insert()
self.target_asset = asset_doc.name
diff --git a/erpnext/assets/doctype/asset_category/asset_category.js b/erpnext/assets/doctype/asset_category/asset_category.js
index c702687..7dde14e 100644
--- a/erpnext/assets/doctype/asset_category/asset_category.js
+++ b/erpnext/assets/doctype/asset_category/asset_category.js
@@ -33,6 +33,7 @@
var d = locals[cdt][cdn];
return {
"filters": {
+ "account_type": "Depreciation",
"root_type": ["in", ["Expense", "Income"]],
"is_group": 0,
"company": d.company_name
diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py
index 2e1def9..8d35141 100644
--- a/erpnext/assets/doctype/asset_category/asset_category.py
+++ b/erpnext/assets/doctype/asset_category/asset_category.py
@@ -53,7 +53,7 @@
account_type_map = {
"fixed_asset_account": {"account_type": ["Fixed Asset"]},
"accumulated_depreciation_account": {"account_type": ["Accumulated Depreciation"]},
- "depreciation_expense_account": {"root_type": ["Expense", "Income"]},
+ "depreciation_expense_account": {"account_type": ["Depreciation"]},
"capital_work_in_progress_account": {"account_type": ["Capital Work in Progress"]},
}
for d in self.accounts:
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
index 39ebd4e..83350aa 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
@@ -107,7 +107,7 @@
have_asset_details_been_modified, not_manual_depr_or_have_manual_depr_details_been_modified
):
self.make_depr_schedule(asset_doc, row, date_of_disposal, update_asset_finance_book_row)
- self.set_accumulated_depreciation(row, date_of_disposal, date_of_return)
+ self.set_accumulated_depreciation(asset_doc, row, date_of_disposal, date_of_return)
def have_asset_details_been_modified(self, asset_doc):
return (
@@ -157,7 +157,12 @@
self.status = "Draft"
def make_depr_schedule(
- self, asset_doc, row, date_of_disposal, update_asset_finance_book_row=True
+ self,
+ asset_doc,
+ row,
+ date_of_disposal,
+ update_asset_finance_book_row=True,
+ value_after_depreciation=None,
):
if not self.get("depreciation_schedule"):
self.depreciation_schedule = []
@@ -167,7 +172,9 @@
start = self.clear_depr_schedule()
- self._make_depr_schedule(asset_doc, row, start, date_of_disposal, update_asset_finance_book_row)
+ self._make_depr_schedule(
+ asset_doc, row, start, date_of_disposal, update_asset_finance_book_row, value_after_depreciation
+ )
def clear_depr_schedule(self):
start = 0
@@ -187,23 +194,30 @@
return start
def _make_depr_schedule(
- self, asset_doc, row, start, date_of_disposal, update_asset_finance_book_row
+ self,
+ asset_doc,
+ row,
+ start,
+ date_of_disposal,
+ update_asset_finance_book_row,
+ value_after_depreciation,
):
asset_doc.validate_asset_finance_books(row)
- value_after_depreciation = _get_value_after_depreciation_for_making_schedule(asset_doc, row)
+ if not value_after_depreciation:
+ value_after_depreciation = _get_value_after_depreciation_for_making_schedule(asset_doc, row)
row.value_after_depreciation = value_after_depreciation
if update_asset_finance_book_row:
row.db_update()
- number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint(
+ final_number_of_depreciations = cint(row.total_number_of_depreciations) - cint(
self.number_of_depreciations_booked
)
has_pro_rata = _check_is_pro_rata(asset_doc, row)
if has_pro_rata:
- number_of_pending_depreciations += 1
+ final_number_of_depreciations += 1
has_wdv_or_dd_non_yearly_pro_rata = False
if (
@@ -219,7 +233,9 @@
depreciation_amount = 0
- for n in range(start, number_of_pending_depreciations):
+ number_of_pending_depreciations = final_number_of_depreciations - start
+
+ for n in range(start, final_number_of_depreciations):
# If depreciation is already completed (for double declining balance)
if skip_row:
continue
@@ -236,10 +252,11 @@
n,
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
+ number_of_pending_depreciations,
)
if not has_pro_rata or (
- n < (cint(number_of_pending_depreciations) - 1) or number_of_pending_depreciations == 2
+ n < (cint(final_number_of_depreciations) - 1) or final_number_of_depreciations == 2
):
schedule_date = add_months(
row.depreciation_start_date, n * cint(row.frequency_of_depreciation)
@@ -310,7 +327,7 @@
)
# For last row
- elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
+ elif has_pro_rata and n == cint(final_number_of_depreciations) - 1:
if not asset_doc.flags.increase_in_asset_life:
# In case of increase_in_asset_life, the asset.to_date is already set on asset_repair submission
asset_doc.to_date = add_months(
@@ -343,7 +360,7 @@
# Adjust depreciation amount in the last period based on the expected value after useful life
if row.expected_value_after_useful_life and (
(
- n == cint(number_of_pending_depreciations) - 1
+ n == cint(final_number_of_depreciations) - 1
and value_after_depreciation != row.expected_value_after_useful_life
)
or value_after_depreciation < row.expected_value_after_useful_life
@@ -392,6 +409,7 @@
def set_accumulated_depreciation(
self,
+ asset_doc,
row,
date_of_disposal=None,
date_of_return=None,
@@ -403,13 +421,21 @@
if self.depreciation_method == "Straight Line" or self.depreciation_method == "Manual"
]
- accumulated_depreciation = flt(self.opening_accumulated_depreciation)
+ accumulated_depreciation = None
value_after_depreciation = flt(row.value_after_depreciation)
for i, d in enumerate(self.get("depreciation_schedule")):
if ignore_booked_entry and d.journal_entry:
continue
+ if not accumulated_depreciation:
+ if i > 0 and asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment:
+ accumulated_depreciation = self.get("depreciation_schedule")[
+ i - 1
+ ].accumulated_depreciation_amount
+ else:
+ accumulated_depreciation = flt(self.opening_accumulated_depreciation)
+
depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount"))
value_after_depreciation -= flt(depreciation_amount)
@@ -507,9 +533,12 @@
schedule_idx=0,
prev_depreciation_amount=0,
has_wdv_or_dd_non_yearly_pro_rata=False,
+ number_of_pending_depreciations=0,
):
if fb_row.depreciation_method in ("Straight Line", "Manual"):
- return get_straight_line_or_manual_depr_amount(asset, fb_row, schedule_idx)
+ return get_straight_line_or_manual_depr_amount(
+ asset, fb_row, schedule_idx, number_of_pending_depreciations
+ )
else:
rate_of_depreciation = get_updated_rate_of_depreciation_for_wdv_and_dd(
asset, depreciable_value, fb_row
@@ -529,7 +558,9 @@
return fb_row.rate_of_depreciation
-def get_straight_line_or_manual_depr_amount(asset, row, schedule_idx):
+def get_straight_line_or_manual_depr_amount(
+ asset, row, schedule_idx, number_of_pending_depreciations
+):
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value
if asset.flags.increase_in_asset_life:
return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / (
@@ -540,6 +571,36 @@
return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / flt(
row.total_number_of_depreciations
)
+ # if the Depreciation Schedule is being modified after Asset Value Adjustment due to decrease in asset value
+ elif asset.flags.decrease_in_asset_value_due_to_value_adjustment:
+ if row.daily_depreciation:
+ daily_depr_amount = (
+ flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
+ ) / date_diff(
+ add_months(
+ row.depreciation_start_date,
+ flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
+ * row.frequency_of_depreciation,
+ ),
+ add_months(
+ row.depreciation_start_date,
+ flt(
+ row.total_number_of_depreciations
+ - asset.number_of_depreciations_booked
+ - number_of_pending_depreciations
+ )
+ * row.frequency_of_depreciation,
+ ),
+ )
+ to_date = add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
+ from_date = add_months(
+ row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation
+ )
+ return daily_depr_amount * date_diff(to_date, from_date)
+ else:
+ return (
+ flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
+ ) / number_of_pending_depreciations
# if the Depreciation Schedule is being prepared for the first time
else:
if row.daily_depreciation:
@@ -669,7 +730,12 @@
def make_new_active_asset_depr_schedules_and_cancel_current_ones(
- asset_doc, notes, date_of_disposal=None, date_of_return=None
+ asset_doc,
+ notes,
+ date_of_disposal=None,
+ date_of_return=None,
+ value_after_depreciation=None,
+ ignore_booked_entry=False,
):
for row in asset_doc.get("finance_books"):
current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(
@@ -695,8 +761,12 @@
row.rate_of_depreciation = new_rate_of_depreciation
new_asset_depr_schedule_doc.rate_of_depreciation = new_rate_of_depreciation
- new_asset_depr_schedule_doc.make_depr_schedule(asset_doc, row, date_of_disposal)
- new_asset_depr_schedule_doc.set_accumulated_depreciation(row, date_of_disposal, date_of_return)
+ new_asset_depr_schedule_doc.make_depr_schedule(
+ asset_doc, row, date_of_disposal, value_after_depreciation=value_after_depreciation
+ )
+ new_asset_depr_schedule_doc.set_accumulated_depreciation(
+ asset_doc, row, date_of_disposal, date_of_return, ignore_booked_entry
+ )
new_asset_depr_schedule_doc.notes = notes
@@ -709,9 +779,20 @@
def get_temp_asset_depr_schedule_doc(
asset_doc, row, date_of_disposal=None, date_of_return=None, update_asset_finance_book_row=False
):
- asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule")
+ current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(
+ asset_doc.name, "Active", row.finance_book
+ )
- asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data(
+ if not current_asset_depr_schedule_doc:
+ frappe.throw(
+ _("Asset Depreciation Schedule not found for Asset {0} and Finance Book {1}").format(
+ asset_doc.name, row.finance_book
+ )
+ )
+
+ temp_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
+
+ temp_asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data(
asset_doc,
row,
date_of_disposal,
@@ -719,7 +800,7 @@
update_asset_finance_book_row,
)
- return asset_depr_schedule_doc
+ return temp_asset_depr_schedule_doc
@frappe.whitelist()
diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
index 823b6e9..9be7243 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
+++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
@@ -5,7 +5,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import date_diff, flt, formatdate, get_link_to_form, getdate
+from frappe.utils import flt, formatdate, get_link_to_form, getdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
@@ -14,8 +14,7 @@
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
- get_asset_depr_schedule_doc,
- get_depreciation_amount,
+ make_new_active_asset_depr_schedules_and_cancel_current_ones,
)
@@ -27,7 +26,7 @@
def on_submit(self):
self.make_depreciation_entry()
- self.reschedule_depreciations(self.new_asset_value)
+ self.update_asset(self.new_asset_value)
add_asset_activity(
self.asset,
_("Asset's value adjusted after submission of Asset Value Adjustment {0}").format(
@@ -36,7 +35,7 @@
)
def on_cancel(self):
- self.reschedule_depreciations(self.current_asset_value)
+ self.update_asset(self.current_asset_value)
add_asset_activity(
self.asset,
_("Asset's value adjusted after cancellation of Asset Value Adjustment {0}").format(
@@ -124,73 +123,33 @@
self.db_set("journal_entry", je.name)
- def reschedule_depreciations(self, asset_value):
+ def update_asset(self, asset_value):
asset = frappe.get_doc("Asset", self.asset)
- country = frappe.get_value("Company", self.company, "country")
- for d in asset.finance_books:
- d.value_after_depreciation = asset_value
+ if not asset.calculate_depreciation:
+ asset.value_after_depreciation = asset_value
+ asset.save()
+ return
- current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(
- asset.name, "Active", d.finance_book
+ asset.flags.decrease_in_asset_value_due_to_value_adjustment = True
+
+ if self.docstatus == 1:
+ notes = _(
+ "This schedule was created when Asset {0} was adjusted through Asset Value Adjustment {1}."
+ ).format(
+ get_link_to_form("Asset", asset.name),
+ get_link_to_form(self.get("doctype"), self.get("name")),
+ )
+ elif self.docstatus == 2:
+ notes = _(
+ "This schedule was created when Asset {0}'s Asset Value Adjustment {1} was cancelled."
+ ).format(
+ get_link_to_form("Asset", asset.name),
+ get_link_to_form(self.get("doctype"), self.get("name")),
)
- new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
- new_asset_depr_schedule_doc.status = "Draft"
- new_asset_depr_schedule_doc.docstatus = 0
-
- current_asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True
- current_asset_depr_schedule_doc.cancel()
-
- if self.docstatus == 1:
- notes = _(
- "This schedule was created when Asset {0} was adjusted through Asset Value Adjustment {1}."
- ).format(
- get_link_to_form(asset.doctype, asset.name),
- get_link_to_form(self.get("doctype"), self.get("name")),
- )
- elif self.docstatus == 2:
- notes = _(
- "This schedule was created when Asset {0}'s Asset Value Adjustment {1} was cancelled."
- ).format(
- get_link_to_form(asset.doctype, asset.name),
- get_link_to_form(self.get("doctype"), self.get("name")),
- )
- new_asset_depr_schedule_doc.notes = notes
-
- new_asset_depr_schedule_doc.insert()
-
- depr_schedule = new_asset_depr_schedule_doc.get("depreciation_schedule")
-
- if d.depreciation_method in ("Straight Line", "Manual"):
- end_date = max(s.schedule_date for s in depr_schedule)
- total_days = date_diff(end_date, self.date)
- rate_per_day = flt(d.value_after_depreciation - d.expected_value_after_useful_life) / flt(
- total_days
- )
- from_date = self.date
- else:
- no_of_depreciations = len([s.name for s in depr_schedule if not s.journal_entry])
-
- value_after_depreciation = d.value_after_depreciation
- for data in depr_schedule:
- if not data.journal_entry:
- if d.depreciation_method in ("Straight Line", "Manual"):
- days = date_diff(data.schedule_date, from_date)
- depreciation_amount = days * rate_per_day
- from_date = data.schedule_date
- else:
- depreciation_amount = get_depreciation_amount(asset, value_after_depreciation, d)
-
- if depreciation_amount:
- value_after_depreciation -= flt(depreciation_amount)
- data.depreciation_amount = depreciation_amount
-
- d.db_update()
-
- new_asset_depr_schedule_doc.set_accumulated_depreciation(d, ignore_booked_entry=True)
- for asset_data in depr_schedule:
- if not asset_data.journal_entry:
- asset_data.db_update()
-
- new_asset_depr_schedule_doc.submit()
+ make_new_active_asset_depr_schedules_and_cancel_current_ones(
+ asset, notes, value_after_depreciation=asset_value, ignore_booked_entry=True
+ )
+ asset.flags.ignore_validate_update_after_submit = True
+ asset.save()
diff --git a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py
index 0b3dcba..5d49759 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py
+++ b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py
@@ -4,9 +4,10 @@
import unittest
import frappe
-from frappe.utils import add_days, get_last_day, nowdate
+from frappe.utils import add_days, cstr, get_last_day, getdate, nowdate
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
+from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
from erpnext.assets.doctype.asset.test_asset import create_asset_data
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc,
@@ -49,27 +50,23 @@
def test_asset_depreciation_value_adjustment(self):
pr = make_purchase_receipt(
- item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location"
+ item_code="Macbook Pro", qty=1, rate=120000.0, location="Test Location"
)
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name")
asset_doc = frappe.get_doc("Asset", asset_name)
asset_doc.calculate_depreciation = 1
+ asset_doc.available_for_use_date = "2023-01-15"
+ asset_doc.purchase_date = "2023-01-15"
- month_end_date = get_last_day(nowdate())
- purchase_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15)
-
- asset_doc.available_for_use_date = purchase_date
- asset_doc.purchase_date = purchase_date
- asset_doc.calculate_depreciation = 1
asset_doc.append(
"finance_books",
{
"expected_value_after_useful_life": 200,
"depreciation_method": "Straight Line",
- "total_number_of_depreciations": 3,
- "frequency_of_depreciation": 10,
- "depreciation_start_date": month_end_date,
+ "total_number_of_depreciations": 12,
+ "frequency_of_depreciation": 1,
+ "depreciation_start_date": "2023-01-31",
},
)
asset_doc.submit()
@@ -77,9 +74,15 @@
first_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Active")
+ post_depreciation_entries(getdate("2023-08-21"))
+
current_value = get_asset_value_after_depreciation(asset_doc.name)
+
adj_doc = make_asset_value_adjustment(
- asset=asset_doc.name, current_asset_value=current_value, new_asset_value=50000.0
+ asset=asset_doc.name,
+ current_asset_value=current_value,
+ new_asset_value=50000.0,
+ date="2023-08-21",
)
adj_doc.submit()
@@ -90,8 +93,8 @@
self.assertEquals(first_asset_depr_schedule.status, "Cancelled")
expected_gle = (
- ("_Test Accumulated Depreciations - _TC", 0.0, 50000.0),
- ("_Test Depreciations - _TC", 50000.0, 0.0),
+ ("_Test Accumulated Depreciations - _TC", 0.0, 4625.29),
+ ("_Test Depreciations - _TC", 4625.29, 0.0),
)
gle = frappe.db.sql(
@@ -103,6 +106,29 @@
self.assertSequenceEqual(gle, expected_gle)
+ expected_schedules = [
+ ["2023-01-31", 5474.73, 5474.73],
+ ["2023-02-28", 9983.33, 15458.06],
+ ["2023-03-31", 9983.33, 25441.39],
+ ["2023-04-30", 9983.33, 35424.72],
+ ["2023-05-31", 9983.33, 45408.05],
+ ["2023-06-30", 9983.33, 55391.38],
+ ["2023-07-31", 9983.33, 65374.71],
+ ["2023-08-31", 8300.0, 73674.71],
+ ["2023-09-30", 8300.0, 81974.71],
+ ["2023-10-31", 8300.0, 90274.71],
+ ["2023-11-30", 8300.0, 98574.71],
+ ["2023-12-31", 8300.0, 106874.71],
+ ["2024-01-15", 8300.0, 115174.71],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in second_asset_depr_schedule.get("depreciation_schedule")
+ ]
+
+ self.assertEqual(schedules, expected_schedules)
+
def make_asset_value_adjustment(**args):
args = frappe._dict(args)
diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
index 94c77ea..bf62a8f 100644
--- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
+++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
@@ -7,13 +7,14 @@
import frappe
from frappe import _
from frappe.query_builder.functions import IfNull, Sum
-from frappe.utils import cstr, flt, formatdate, getdate
+from frappe.utils import add_months, cstr, flt, formatdate, getdate, nowdate, today
from erpnext.accounts.report.financial_statements import (
get_fiscal_year_data,
get_period_list,
validate_fiscal_year,
)
+from erpnext.accounts.utils import get_fiscal_year
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
@@ -37,15 +38,26 @@
if filters.get("company"):
conditions["company"] = filters.company
+
if filters.filter_based_on == "Date Range":
+ if not filters.from_date and not filters.to_date:
+ filters.from_date = add_months(nowdate(), -12)
+ filters.to_date = nowdate()
+
conditions[date_field] = ["between", [filters.from_date, filters.to_date]]
- if filters.filter_based_on == "Fiscal Year":
+ elif filters.filter_based_on == "Fiscal Year":
+ if not filters.from_fiscal_year and not filters.to_fiscal_year:
+ default_fiscal_year = get_fiscal_year(today())[0]
+ filters.from_fiscal_year = default_fiscal_year
+ filters.to_fiscal_year = default_fiscal_year
+
fiscal_year = get_fiscal_year_data(filters.from_fiscal_year, filters.to_fiscal_year)
validate_fiscal_year(fiscal_year, filters.from_fiscal_year, filters.to_fiscal_year)
filters.year_start_date = getdate(fiscal_year.year_start_date)
filters.year_end_date = getdate(fiscal_year.year_end_date)
conditions[date_field] = ["between", [filters.year_start_date, filters.year_end_date]]
+
if filters.get("only_existing_assets"):
conditions["is_existing_asset"] = filters.get("only_existing_assets")
if filters.get("asset_category"):
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index 7c33056..f6a1951 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -185,8 +185,7 @@
if(!in_list(["Closed", "Delivered"], doc.status)) {
if(this.frm.doc.status !== 'Closed' && flt(this.frm.doc.per_received, 2) < 100 && flt(this.frm.doc.per_billed, 2) < 100) {
- // Don't add Update Items button if the PO is following the new subcontracting flow.
- if (!(this.frm.doc.is_subcontracted && !this.frm.doc.is_old_subcontracting_flow)) {
+ if (!this.frm.doc.__onload || this.frm.doc.__onload.can_update_items) {
this.frm.add_custom_button(__('Update Items'), () => {
erpnext.utils.update_child_items({
frm: this.frm,
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 06b9d29..3576cd4 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -52,6 +52,7 @@
def onload(self):
supplier_tds = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
self.set_onload("supplier_tds", supplier_tds)
+ self.set_onload("can_update_items", self.can_update_items())
def validate(self):
super(PurchaseOrder, self).validate()
@@ -450,6 +451,17 @@
else:
self.db_set("per_received", 0, update_modified=False)
+ def can_update_items(self) -> bool:
+ result = True
+
+ if self.is_subcontracted and not self.is_old_subcontracting_flow:
+ if frappe.db.exists(
+ "Subcontracting Order", {"purchase_order": self.name, "docstatus": ["!=", 2]}
+ ):
+ result = False
+
+ return result
+
def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0):
"""get last purchase rate for an item"""
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index 3edaffa..55c01e8 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -901,6 +901,71 @@
self.assertRaises(frappe.ValidationError, po.save)
+ def test_update_items_for_subcontracting_purchase_order(self):
+ from erpnext.controllers.tests.test_subcontracting_controller import (
+ get_subcontracting_order,
+ make_bom_for_subcontracted_items,
+ make_raw_materials,
+ make_service_items,
+ make_subcontracted_items,
+ )
+
+ def update_items(po, qty):
+ trans_items = [po.items[0].as_dict()]
+ trans_items[0]["qty"] = qty
+ trans_items[0]["fg_item_qty"] = qty
+ trans_items = json.dumps(trans_items, default=str)
+
+ return update_child_qty_rate(
+ po.doctype,
+ trans_items,
+ po.name,
+ )
+
+ make_subcontracted_items()
+ make_raw_materials()
+ make_service_items()
+ make_bom_for_subcontracted_items()
+
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 7",
+ "qty": 10,
+ "rate": 100,
+ "fg_item": "Subcontracted Item SA7",
+ "fg_item_qty": 10,
+ },
+ ]
+ po = create_purchase_order(
+ rm_items=service_items,
+ is_subcontracted=1,
+ supplier_warehouse="_Test Warehouse 1 - _TC",
+ )
+
+ update_items(po, qty=20)
+ po.reload()
+
+ # Test - 1: Items should be updated as there is no Subcontracting Order against PO
+ self.assertEqual(po.items[0].qty, 20)
+ self.assertEqual(po.items[0].fg_item_qty, 20)
+
+ sco = get_subcontracting_order(po_name=po.name, warehouse="_Test Warehouse - _TC")
+
+ # Test - 2: ValidationError should be raised as there is Subcontracting Order against PO
+ self.assertRaises(frappe.ValidationError, update_items, po=po, qty=30)
+
+ sco.reload()
+ sco.cancel()
+ po.reload()
+
+ update_items(po, qty=30)
+ po.reload()
+
+ # Test - 3: Items should be updated as the Subcontracting Order is cancelled
+ self.assertEqual(po.items[0].qty, 30)
+ self.assertEqual(po.items[0].fg_item_qty, 30)
+
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
index fbfc1ac..06dbd86 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
@@ -25,6 +25,7 @@
"col_break_email_1",
"html_llwp",
"send_attached_files",
+ "send_document_print",
"sec_break_email_2",
"message_for_supplier",
"terms_section_break",
@@ -283,13 +284,21 @@
"fieldname": "send_attached_files",
"fieldtype": "Check",
"label": "Send Attached Files"
+ },
+ {
+ "default": "0",
+ "description": "If enabled, a print of this document will be attached to each email",
+ "fieldname": "send_document_print",
+ "fieldtype": "Check",
+ "label": "Send Document Print",
+ "print_hide": 1
}
],
"icon": "fa fa-shopping-cart",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2023-08-08 16:30:10.870429",
+ "modified": "2023-08-09 12:20:26.850623",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
index e938577..6b39982 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
@@ -193,7 +193,7 @@
"supplier": data.get("supplier"),
"supplier_name": data.get("supplier_name"),
"update_password_link": f'<a href="{update_password_link}" class="btn btn-default btn-xs" target="_blank">{_("Set Password")}</a>',
- "portal_link": f'<a href="{rfq_link}" class="btn btn-default btn-sm" target="_blank"> {_("Submit your Quotation")} </a>',
+ "portal_link": f'<a href="{rfq_link}" class="btn btn-default btn-xs" target="_blank"> {_("Submit your Quotation")} </a>',
"user_fullname": full_name,
}
)
@@ -205,10 +205,24 @@
if preview:
return {"message": message, "subject": subject}
- attachments = None
+ attachments = []
if self.send_attached_files:
attachments = self.get_attachments()
+ if self.send_document_print:
+ supplier_language = frappe.db.get_value("Supplier", data.supplier, "language")
+ system_language = frappe.db.get_single_value("System Settings", "language")
+ attachments.append(
+ frappe.attach_print(
+ self.doctype,
+ self.name,
+ doc=self,
+ print_format=self.meta.default_print_format or "Standard",
+ lang=supplier_language or system_language,
+ letterhead=self.letter_head,
+ )
+ )
+
self.send_email(data, sender, subject, message, attachments)
def send_email(self, data, sender, subject, message, attachments):
@@ -218,7 +232,6 @@
recipients=data.email_id,
sender=sender,
attachments=attachments,
- print_format=self.meta.default_print_format or "Standard",
send_email=True,
doctype=self.doctype,
name=self.name,
diff --git a/erpnext/buying/report/procurement_tracker/procurement_tracker.py b/erpnext/buying/report/procurement_tracker/procurement_tracker.py
index 71019e8..a7e03c0 100644
--- a/erpnext/buying/report/procurement_tracker/procurement_tracker.py
+++ b/erpnext/buying/report/procurement_tracker/procurement_tracker.py
@@ -154,31 +154,35 @@
procurement_record = []
if procurement_record_against_mr:
procurement_record += procurement_record_against_mr
+
for po in purchase_order_entry:
# fetch material records linked to the purchase order item
- mr_record = mr_records.get(po.material_request_item, [{}])[0]
- procurement_detail = {
- "material_request_date": mr_record.get("transaction_date"),
- "cost_center": po.cost_center,
- "project": po.project,
- "requesting_site": po.warehouse,
- "requestor": po.owner,
- "material_request_no": po.material_request,
- "item_code": po.item_code,
- "quantity": flt(po.qty),
- "unit_of_measurement": po.stock_uom,
- "status": po.status,
- "purchase_order_date": po.transaction_date,
- "purchase_order": po.parent,
- "supplier": po.supplier,
- "estimated_cost": flt(mr_record.get("amount")),
- "actual_cost": flt(pi_records.get(po.name)),
- "purchase_order_amt": flt(po.amount),
- "purchase_order_amt_in_company_currency": flt(po.base_amount),
- "expected_delivery_date": po.schedule_date,
- "actual_delivery_date": pr_records.get(po.name),
- }
- procurement_record.append(procurement_detail)
+ material_requests = mr_records.get(po.material_request_item, [{}])
+
+ for mr_record in material_requests:
+ procurement_detail = {
+ "material_request_date": mr_record.get("transaction_date"),
+ "cost_center": po.cost_center,
+ "project": po.project,
+ "requesting_site": po.warehouse,
+ "requestor": po.owner,
+ "material_request_no": po.material_request,
+ "item_code": po.item_code,
+ "quantity": flt(po.qty),
+ "unit_of_measurement": po.stock_uom,
+ "status": po.status,
+ "purchase_order_date": po.transaction_date,
+ "purchase_order": po.parent,
+ "supplier": po.supplier,
+ "estimated_cost": flt(mr_record.get("amount")),
+ "actual_cost": flt(pi_records.get(po.name)),
+ "purchase_order_amt": flt(po.amount),
+ "purchase_order_amt_in_company_currency": flt(po.base_amount),
+ "expected_delivery_date": po.schedule_date,
+ "actual_delivery_date": pr_records.get(po.name),
+ }
+ procurement_record.append(procurement_detail)
+
return procurement_record
@@ -301,7 +305,7 @@
& (parent.name == child.parent)
& (parent.status.notin(("Closed", "Completed", "Cancelled")))
)
- .groupby(parent.name, child.item_code)
+ .groupby(parent.name, child.material_request_item)
)
query = apply_filters_on_query(filters, parent, child, query)
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index fbf97aa..955ebef 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -716,7 +716,9 @@
def validate_enabled_taxes_and_charges(self):
taxes_and_charges_doctype = self.meta.get_options("taxes_and_charges")
- if frappe.get_cached_value(taxes_and_charges_doctype, self.taxes_and_charges, "disabled"):
+ if self.taxes_and_charges and frappe.get_cached_value(
+ taxes_and_charges_doctype, self.taxes_and_charges, "disabled"
+ ):
frappe.throw(
_("{0} '{1}' is disabled").format(taxes_and_charges_doctype, self.taxes_and_charges)
)
@@ -803,8 +805,28 @@
gl_dict, account_currency, self.get("conversion_rate"), self.company_currency
)
+ # Update details in transaction currency
+ gl_dict.update(
+ {
+ "transaction_currency": self.get("currency") or self.company_currency,
+ "transaction_exchange_rate": self.get("conversion_rate", 1),
+ "debit_in_transaction_currency": self.get_value_in_transaction_currency(
+ account_currency, args, "debit"
+ ),
+ "credit_in_transaction_currency": self.get_value_in_transaction_currency(
+ account_currency, args, "credit"
+ ),
+ }
+ )
+
return gl_dict
+ def get_value_in_transaction_currency(self, account_currency, args, field):
+ if account_currency == self.get("currency"):
+ return args.get(field + "_in_account_currency")
+ else:
+ return flt(args.get(field, 0) / self.get("conversion_rate", 1))
+
def validate_qty_is_not_zero(self):
if self.doctype != "Purchase Receipt":
for item in self.items:
@@ -2396,6 +2418,9 @@
q = q.select((payment_entry.target_exchange_rate).as_("exchange_rate"))
if condition:
+ if condition.get("name", None):
+ q = q.where(payment_entry.name.like(f"%{condition.get('name')}%"))
+
q = q.where(payment_entry.company == condition["company"])
q = (
q.where(payment_entry.posting_date >= condition["from_payment_date"])
@@ -2833,6 +2858,27 @@
return update_supplied_items
+ def validate_fg_item_for_subcontracting(new_data, is_new):
+ if is_new:
+ if not new_data.get("fg_item"):
+ frappe.throw(
+ _("Finished Good Item is not specified for service item {0}").format(new_data["item_code"])
+ )
+ else:
+ is_sub_contracted_item, default_bom = frappe.db.get_value(
+ "Item", new_data["fg_item"], ["is_sub_contracted_item", "default_bom"]
+ )
+
+ if not is_sub_contracted_item:
+ frappe.throw(
+ _("Finished Good Item {0} must be a sub-contracted item").format(new_data["fg_item"])
+ )
+ elif not default_bom:
+ frappe.throw(_("Default BOM not found for FG Item {0}").format(new_data["fg_item"]))
+
+ if not new_data.get("fg_item_qty"):
+ frappe.throw(_("Finished Good Item {0} Qty can not be zero").format(new_data["fg_item"]))
+
data = json.loads(trans_items)
any_qty_changed = False # updated to true if any item's qty changes
@@ -2864,6 +2910,7 @@
prev_rate, new_rate = flt(child_item.get("rate")), flt(d.get("rate"))
prev_qty, new_qty = flt(child_item.get("qty")), flt(d.get("qty"))
+ prev_fg_qty, new_fg_qty = flt(child_item.get("fg_item_qty")), flt(d.get("fg_item_qty"))
prev_con_fac, new_con_fac = flt(child_item.get("conversion_factor")), flt(
d.get("conversion_factor")
)
@@ -2876,6 +2923,7 @@
rate_unchanged = prev_rate == new_rate
qty_unchanged = prev_qty == new_qty
+ fg_qty_unchanged = prev_fg_qty == new_fg_qty
uom_unchanged = prev_uom == new_uom
conversion_factor_unchanged = prev_con_fac == new_con_fac
any_conversion_factor_changed |= not conversion_factor_unchanged
@@ -2885,6 +2933,7 @@
if (
rate_unchanged
and qty_unchanged
+ and fg_qty_unchanged
and conversion_factor_unchanged
and uom_unchanged
and date_unchanged
@@ -2895,6 +2944,17 @@
if flt(child_item.get("qty")) != flt(d.get("qty")):
any_qty_changed = True
+ if (
+ parent.doctype == "Purchase Order"
+ and parent.is_subcontracted
+ and not parent.is_old_subcontracting_flow
+ ):
+ validate_fg_item_for_subcontracting(d, new_child_flag)
+ child_item.fg_item_qty = flt(d["fg_item_qty"])
+
+ if new_child_flag:
+ child_item.fg_item = d["fg_item"]
+
child_item.qty = flt(d.get("qty"))
rate_precision = child_item.precision("rate") or 2
conv_fac_precision = child_item.precision("conversion_factor") or 2
@@ -2998,11 +3058,20 @@
parent.update_ordered_qty()
parent.update_ordered_and_reserved_qty()
parent.update_receiving_percentage()
- if parent.is_old_subcontracting_flow:
- if should_update_supplied_items(parent):
- parent.update_reserved_qty_for_subcontract()
- parent.create_raw_materials_supplied()
- parent.save()
+
+ if parent.is_subcontracted:
+ if parent.is_old_subcontracting_flow:
+ if should_update_supplied_items(parent):
+ parent.update_reserved_qty_for_subcontract()
+ parent.create_raw_materials_supplied()
+ parent.save()
+ else:
+ if not parent.can_update_items():
+ frappe.throw(
+ _(
+ "Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}."
+ ).format(frappe.bold(parent.name))
+ )
else: # Sales Order
parent.validate_warehouse()
parent.update_reserved_qty()
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 7b7c53e..b396b27 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -759,7 +759,7 @@
"company": self.company,
"supplier": self.supplier,
"purchase_date": self.posting_date,
- "calculate_depreciation": 1,
+ "calculate_depreciation": 0,
"purchase_receipt_amount": purchase_amount,
"gross_purchase_amount": purchase_amount,
"asset_quantity": row.qty if is_grouped_asset else 0,
diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py
index a4bc4a9..73a248f 100644
--- a/erpnext/controllers/status_updater.py
+++ b/erpnext/controllers/status_updater.py
@@ -5,7 +5,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import comma_or, flt, getdate, now, nowdate
+from frappe.utils import comma_or, flt, get_link_to_form, getdate, now, nowdate
class OverAllowanceError(frappe.ValidationError):
@@ -233,8 +233,17 @@
if hasattr(d, "qty") and d.qty > 0 and self.get("is_return"):
frappe.throw(_("For an item {0}, quantity must be negative number").format(d.item_code))
- if hasattr(d, "item_code") and hasattr(d, "rate") and d.rate < 0:
- frappe.throw(_("For an item {0}, rate must be a positive number").format(d.item_code))
+ if not frappe.db.get_single_value("Selling Settings", "allow_negative_rates_for_items"):
+ if hasattr(d, "item_code") and hasattr(d, "rate") and flt(d.rate) < 0:
+ frappe.throw(
+ _(
+ "For item {0}, rate must be a positive number. To Allow negative rates, enable {1} in {2}"
+ ).format(
+ frappe.bold(d.item_code),
+ frappe.bold(_("`Allow Negative rates for Items`")),
+ get_link_to_form("Selling Settings", "Selling Settings"),
+ ),
+ )
if d.doctype == args["source_dt"] and d.get(args["join_field"]):
args["name"] = d.get(args["join_field"])
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index d669abe..ae54b80 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -599,6 +599,7 @@
inspection_fieldname_map = {
"Purchase Receipt": "inspection_required_before_purchase",
"Purchase Invoice": "inspection_required_before_purchase",
+ "Subcontracting Receipt": "inspection_required_before_purchase",
"Sales Invoice": "inspection_required_before_delivery",
"Delivery Note": "inspection_required_before_delivery",
}
diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py
index eeb35c4..6b61ae9 100644
--- a/erpnext/controllers/tests/test_subcontracting_controller.py
+++ b/erpnext/controllers/tests/test_subcontracting_controller.py
@@ -1090,7 +1090,7 @@
po = frappe.get_doc("Purchase Order", args.get("po_name"))
if po.is_subcontracted:
- return create_subcontracting_order(po_name=po.name, **args)
+ return create_subcontracting_order(**args)
if not args.service_items:
service_items = [
diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js
index 6ef8297..0b485bb 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.js
+++ b/erpnext/crm/doctype/opportunity/opportunity.js
@@ -1,7 +1,7 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.crm");
-erpnext.pre_sales.set_as_lost("Quotation");
+erpnext.pre_sales.set_as_lost("Opportunity");
erpnext.sales_common.setup_selling_controller();
diff --git a/erpnext/e_commerce/web_template/hero_slider/hero_slider.html b/erpnext/e_commerce/web_template/hero_slider/hero_slider.html
index e560f4a..fe4fee3 100644
--- a/erpnext/e_commerce/web_template/hero_slider/hero_slider.html
+++ b/erpnext/e_commerce/web_template/hero_slider/hero_slider.html
@@ -1,7 +1,7 @@
{%- macro slide(image, title, subtitle, action, label, index, align="Left", theme="Dark") -%}
{%- set align_class = resolve_class({
'text-right': align == 'Right',
- 'text-centre': align == 'Centre',
+ 'text-center': align == 'Centre',
'text-left': align == 'Left',
}) -%}
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
index 61d2ace..11d5f6a 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
@@ -237,14 +237,15 @@
deposit = abs(amount)
withdrawal = 0.0
- status = "Pending" if transaction["pending"] == "True" else "Settled"
+ status = "Pending" if transaction["pending"] == True else "Settled"
tags = []
- try:
- tags += transaction["category"]
- tags += [f'Plaid Cat. {transaction["category_id"]}']
- except KeyError:
- pass
+ if transaction["category"]:
+ try:
+ tags += transaction["category"]
+ tags += [f'Plaid Cat. {transaction["category_id"]}']
+ except KeyError:
+ pass
if not frappe.db.exists("Bank Transaction", dict(transaction_id=transaction["transaction_id"])):
try:
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 44c68dc..7eaa146 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -70,6 +70,19 @@
"Department",
]
+demo_master_doctypes = [
+ "item_group",
+ "item",
+ "customer_group",
+ "supplier_group",
+ "customer",
+ "supplier",
+]
+demo_transaction_doctypes = [
+ "purchase_order",
+ "sales_order",
+]
+
jinja = {
"methods": [
"erpnext.stock.serial_batch_bundle.get_serial_or_batch_nos",
diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json
index d024022..e8d3542 100644
--- a/erpnext/manufacturing/doctype/bom/bom.json
+++ b/erpnext/manufacturing/doctype/bom/bom.json
@@ -78,6 +78,10 @@
"show_items",
"show_operations",
"web_long_description",
+ "reference_section",
+ "bom_creator",
+ "bom_creator_item",
+ "column_break_oxbz",
"amended_from",
"connections_tab"
],
@@ -233,7 +237,7 @@
"fieldname": "rm_cost_as_per",
"fieldtype": "Select",
"label": "Rate Of Materials Based On",
- "options": "Valuation Rate\nLast Purchase Rate\nPrice List"
+ "options": "Valuation Rate\nLast Purchase Rate\nPrice List\nManual"
},
{
"allow_on_submit": 1,
@@ -599,6 +603,32 @@
"fieldname": "operating_cost_per_bom_quantity",
"fieldtype": "Currency",
"label": "Operating Cost Per BOM Quantity"
+ },
+ {
+ "fieldname": "reference_section",
+ "fieldtype": "Section Break",
+ "label": "Reference"
+ },
+ {
+ "fieldname": "bom_creator",
+ "fieldtype": "Link",
+ "label": "BOM Creator",
+ "no_copy": 1,
+ "options": "BOM Creator",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "bom_creator_item",
+ "fieldtype": "Data",
+ "label": "BOM Creator Item",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_oxbz",
+ "fieldtype": "Column Break"
}
],
"icon": "fa fa-sitemap",
@@ -606,7 +636,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2023-04-06 12:47:58.514795",
+ "modified": "2023-08-07 11:38:08.152294",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 8058a5f..0231668 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -206,6 +206,7 @@
def on_submit(self):
self.manage_default_bom()
+ self.update_bom_creator_status()
def on_cancel(self):
self.db_set("is_active", 0)
@@ -214,6 +215,23 @@
# check if used in any other bom
self.validate_bom_links()
self.manage_default_bom()
+ self.update_bom_creator_status()
+
+ def update_bom_creator_status(self):
+ if not self.bom_creator:
+ return
+
+ if self.bom_creator_item:
+ frappe.db.set_value(
+ "BOM Creator Item",
+ self.bom_creator_item,
+ "bom_created",
+ 1 if self.docstatus == 1 else 0,
+ update_modified=False,
+ )
+
+ doc = frappe.get_doc("BOM Creator", self.bom_creator)
+ doc.set_status(save=True)
def on_update_after_submit(self):
self.validate_bom_links()
@@ -662,18 +680,19 @@
for d in self.get("items"):
old_rate = d.rate
- d.rate = self.get_rm_rate(
- {
- "company": self.company,
- "item_code": d.item_code,
- "bom_no": d.bom_no,
- "qty": d.qty,
- "uom": d.uom,
- "stock_uom": d.stock_uom,
- "conversion_factor": d.conversion_factor,
- "sourced_by_supplier": d.sourced_by_supplier,
- }
- )
+ if self.rm_cost_as_per != "Manual":
+ d.rate = self.get_rm_rate(
+ {
+ "company": self.company,
+ "item_code": d.item_code,
+ "bom_no": d.bom_no,
+ "qty": d.qty,
+ "uom": d.uom,
+ "stock_uom": d.stock_uom,
+ "conversion_factor": d.conversion_factor,
+ "sourced_by_supplier": d.sourced_by_supplier,
+ }
+ )
d.base_rate = flt(d.rate) * flt(self.conversion_rate)
d.amount = flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty"))
@@ -964,7 +983,12 @@
.as_("valuation_rate")
)
.where((bin_table.item_code == item_code) & (wh_table.company == company))
- ).run(as_dict=True)[0]
+ )
+
+ if data.get("set_rate_based_on_warehouse") and data.get("warehouse"):
+ item_valuation = item_valuation.where(bin_table.warehouse == data.get("warehouse"))
+
+ item_valuation = item_valuation.run(as_dict=True)[0]
valuation_rate = item_valuation.get("valuation_rate")
diff --git a/erpnext/manufacturing/doctype/bom_creator/__init__.py b/erpnext/manufacturing/doctype/bom_creator/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_creator/__init__.py
diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.js b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js
new file mode 100644
index 0000000..01dc89b
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js
@@ -0,0 +1,201 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+frappe.provide("erpnext.bom");
+
+frappe.ui.form.on("BOM Creator", {
+ setup(frm) {
+ frm.trigger("set_queries");
+ },
+
+ setup_bom_creator(frm) {
+ frm.dashboard.clear_comment();
+
+ if (!frm.is_new()) {
+ if ((!frappe.bom_configurator
+ || frappe.bom_configurator.bom_configurator !== frm.doc.name)) {
+ frm.trigger("build_tree");
+ }
+ } else {
+ let $parent = $(frm.fields_dict["bom_creator"].wrapper);
+ $parent.empty();
+ frm.trigger("make_new_entry");
+ }
+ },
+
+ build_tree(frm) {
+ let $parent = $(frm.fields_dict["bom_creator"].wrapper);
+ $parent.empty();
+ frm.toggle_enable("item_code", false);
+
+ frappe.require('bom_configurator.bundle.js').then(() => {
+ frappe.bom_configurator = new frappe.ui.BOMConfigurator({
+ wrapper: $parent,
+ page: $parent,
+ frm: frm,
+ bom_configurator: frm.doc.name,
+ });
+ });
+ },
+
+ make_new_entry(frm) {
+ let dialog = new frappe.ui.Dialog({
+ title: __("Multi-level BOM Creator"),
+ fields: [
+ {
+ label: __("Name"),
+ fieldtype: "Data",
+ fieldname: "name",
+ reqd: 1
+ },
+ { fieldtype: "Column Break" },
+ {
+ label: __("Company"),
+ fieldtype: "Link",
+ fieldname: "company",
+ options: "Company",
+ reqd: 1,
+ default: frappe.defaults.get_user_default("Company"),
+ },
+ { fieldtype: "Section Break" },
+ {
+ label: __("Item Code (Final Product)"),
+ fieldtype: "Link",
+ fieldname: "item_code",
+ options: "Item",
+ reqd: 1
+ },
+ { fieldtype: "Column Break" },
+ {
+ label: __("Quantity"),
+ fieldtype: "Float",
+ fieldname: "qty",
+ reqd: 1,
+ default: 1.0
+ },
+ { fieldtype: "Section Break" },
+ {
+ label: __("Currency"),
+ fieldtype: "Link",
+ fieldname: "currency",
+ options: "Currency",
+ reqd: 1,
+ default: frappe.defaults.get_global_default("currency")
+ },
+ { fieldtype: "Column Break" },
+ {
+ label: __("Conversion Rate"),
+ fieldtype: "Float",
+ fieldname: "conversion_rate",
+ reqd: 1,
+ default: 1.0
+ },
+ ],
+ primary_action_label: __("Create"),
+ primary_action: (values) => {
+ values.doctype = frm.doc.doctype;
+ frappe.db
+ .insert(values)
+ .then((doc) => {
+ frappe.set_route("Form", doc.doctype, doc.name);
+ });
+ }
+ })
+
+ dialog.show();
+ },
+
+ set_queries(frm) {
+ frm.set_query("bom_no", "items", function(doc, cdt, cdn) {
+ let item = frappe.get_doc(cdt, cdn);
+ return {
+ filters: {
+ item: item.item_code,
+ }
+ }
+ });
+ },
+
+ refresh(frm) {
+ frm.trigger("setup_bom_creator");
+ frm.trigger("set_root_item");
+ frm.trigger("add_custom_buttons");
+ },
+
+ set_root_item(frm) {
+ if (frm.is_new() && frm.doc.items?.length) {
+ frappe.model.set_value(frm.doc.items[0].doctype,
+ frm.doc.items[0].name, "is_root", 1);
+ }
+ },
+
+ add_custom_buttons(frm) {
+ if (!frm.is_new()) {
+ frm.add_custom_button(__("Rebuild Tree"), () => {
+ frm.trigger("build_tree");
+ });
+ }
+ }
+});
+
+frappe.ui.form.on("BOM Creator Item", {
+ item_code(frm, cdt, cdn) {
+ let item = frappe.get_doc(cdt, cdn);
+ if (item.item_code && item.is_root) {
+ frappe.model.set_value(cdt, cdn, "fg_item", item.item_code);
+ }
+ },
+
+ do_not_explode(frm, cdt, cdn) {
+ let item = frappe.get_doc(cdt, cdn);
+ if (!item.do_not_explode) {
+ frm.call({
+ method: "get_default_bom",
+ doc: frm.doc,
+ args: {
+ item_code: item.item_code
+ },
+ callback(r) {
+ if (r.message) {
+ frappe.model.set_value(cdt, cdn, "bom_no", r.message);
+ }
+ }
+ })
+ } else {
+ frappe.model.set_value(cdt, cdn, "bom_no", "");
+ }
+ }
+});
+
+
+erpnext.bom.BomConfigurator = class BomConfigurator extends erpnext.TransactionController {
+ conversion_rate(doc) {
+ if(this.frm.doc.currency === this.get_company_currency()) {
+ this.frm.set_value("conversion_rate", 1.0);
+ } else {
+ erpnext.bom.update_cost(doc);
+ }
+ }
+
+ buying_price_list(doc) {
+ this.apply_price_list();
+ }
+
+ plc_conversion_rate(doc) {
+ if (!this.in_apply_price_list) {
+ this.apply_price_list(null, true);
+ }
+ }
+
+ conversion_factor(doc, cdt, cdn) {
+ if (frappe.meta.get_docfield(cdt, "stock_qty", cdn)) {
+ var item = frappe.get_doc(cdt, cdn);
+ frappe.model.round_floats_in(item, ["qty", "conversion_factor"]);
+ item.stock_qty = flt(item.qty * item.conversion_factor, precision("stock_qty", item));
+ refresh_field("stock_qty", item.name, item.parentfield);
+ this.toggle_conversion_factor(item);
+ this.frm.events.update_cost(this.frm);
+ }
+ }
+};
+
+extend_cscript(cur_frm.cscript, new erpnext.bom.BomConfigurator({frm: cur_frm}));
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.json b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json
new file mode 100644
index 0000000..fb4c6c5
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json
@@ -0,0 +1,330 @@
+{
+ "actions": [],
+ "allow_import": 1,
+ "autoname": "prompt",
+ "creation": "2023-07-18 14:56:34.477800",
+ "default_view": "List",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "engine": "InnoDB",
+ "field_order": [
+ "tab_2_tab",
+ "bom_creator",
+ "details_tab",
+ "section_break_ylsl",
+ "item_code",
+ "item_name",
+ "item_group",
+ "column_break_ikj7",
+ "qty",
+ "project",
+ "uom",
+ "raw_materials_tab",
+ "currency_detail",
+ "rm_cost_as_per",
+ "set_rate_based_on_warehouse",
+ "buying_price_list",
+ "price_list_currency",
+ "plc_conversion_rate",
+ "column_break_ivyw",
+ "currency",
+ "conversion_rate",
+ "section_break_zcfg",
+ "default_warehouse",
+ "column_break_tzot",
+ "company",
+ "materials_section",
+ "items",
+ "costing_detail",
+ "raw_material_cost",
+ "remarks_tab",
+ "remarks",
+ "section_break_yixm",
+ "status",
+ "column_break_irab",
+ "error_log",
+ "connections_tab",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "remember_last_selected_value": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "currency_detail",
+ "fieldtype": "Section Break",
+ "label": "Costing"
+ },
+ {
+ "allow_on_submit": 1,
+ "default": "Valuation Rate",
+ "fieldname": "rm_cost_as_per",
+ "fieldtype": "Select",
+ "label": "Rate Of Materials Based On",
+ "options": "Valuation Rate\nLast Purchase Rate\nPrice List\nManual",
+ "reqd": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "depends_on": "eval:doc.rm_cost_as_per===\"Price List\"",
+ "fieldname": "buying_price_list",
+ "fieldtype": "Link",
+ "label": "Price List",
+ "options": "Price List"
+ },
+ {
+ "allow_on_submit": 1,
+ "depends_on": "eval:doc.rm_cost_as_per=='Price List'",
+ "fieldname": "price_list_currency",
+ "fieldtype": "Link",
+ "label": "Price List Currency",
+ "options": "Currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "depends_on": "eval:doc.rm_cost_as_per=='Price List'",
+ "fieldname": "plc_conversion_rate",
+ "fieldtype": "Float",
+ "label": "Price List Exchange Rate"
+ },
+ {
+ "fieldname": "column_break_ivyw",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "currency",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Currency",
+ "options": "Currency",
+ "reqd": 1
+ },
+ {
+ "default": "1",
+ "fieldname": "conversion_rate",
+ "fieldtype": "Float",
+ "label": "Conversion Rate",
+ "precision": "9"
+ },
+ {
+ "fieldname": "materials_section",
+ "fieldtype": "Section Break",
+ "oldfieldtype": "Section Break"
+ },
+ {
+ "allow_bulk_edit": 1,
+ "fieldname": "items",
+ "fieldtype": "Table",
+ "label": "Items",
+ "oldfieldname": "bom_materials",
+ "oldfieldtype": "Table",
+ "options": "BOM Creator Item"
+ },
+ {
+ "fieldname": "costing_detail",
+ "fieldtype": "Section Break",
+ "label": "Costing Details"
+ },
+ {
+ "fieldname": "raw_material_cost",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Total Cost",
+ "no_copy": 1,
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "remarks",
+ "fieldtype": "Text Editor",
+ "label": "Remarks"
+ },
+ {
+ "fieldname": "column_break_ikj7",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "project",
+ "fieldtype": "Link",
+ "label": "Project",
+ "options": "Project"
+ },
+ {
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Finished Good",
+ "options": "Item",
+ "reqd": 1
+ },
+ {
+ "fieldname": "qty",
+ "fieldtype": "Float",
+ "label": "Quantity",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "item_code.item_name",
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "label": "Item Name"
+ },
+ {
+ "fetch_from": "item_code.stock_uom",
+ "fieldname": "uom",
+ "fieldtype": "Link",
+ "label": "UOM",
+ "options": "UOM"
+ },
+ {
+ "fieldname": "tab_2_tab",
+ "fieldtype": "Tab Break",
+ "label": "BOM Tree"
+ },
+ {
+ "fieldname": "details_tab",
+ "fieldtype": "Tab Break",
+ "label": "Final Product"
+ },
+ {
+ "fieldname": "raw_materials_tab",
+ "fieldtype": "Tab Break",
+ "label": "Sub Assemblies & Raw Materials"
+ },
+ {
+ "fieldname": "remarks_tab",
+ "fieldtype": "Tab Break",
+ "label": "Remarks"
+ },
+ {
+ "fieldname": "connections_tab",
+ "fieldtype": "Tab Break",
+ "label": "Connections",
+ "show_dashboard": 1
+ },
+ {
+ "fetch_from": "item_code.item_group",
+ "fieldname": "item_group",
+ "fieldtype": "Link",
+ "label": "Item Group",
+ "options": "Item Group"
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "BOM Creator",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_zcfg",
+ "fieldtype": "Section Break",
+ "label": "Warehouse"
+ },
+ {
+ "fieldname": "column_break_tzot",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "default_warehouse",
+ "fieldtype": "Link",
+ "label": "Default Source Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "fieldname": "bom_creator",
+ "fieldtype": "HTML"
+ },
+ {
+ "fieldname": "section_break_ylsl",
+ "fieldtype": "Section Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.rm_cost_as_per === \"Valuation Rate\"",
+ "fieldname": "set_rate_based_on_warehouse",
+ "fieldtype": "Check",
+ "label": "Set Valuation Rate Based on Source Warehouse"
+ },
+ {
+ "fieldname": "section_break_yixm",
+ "fieldtype": "Section Break"
+ },
+ {
+ "default": "Draft",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "label": "Status",
+ "no_copy": 1,
+ "options": "Draft\nSubmitted\nIn Progress\nCompleted\nFailed\nCancelled",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_irab",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "error_log",
+ "fieldtype": "Text",
+ "label": "Error Log",
+ "read_only": 1
+ }
+ ],
+ "icon": "fa fa-sitemap",
+ "is_submittable": 1,
+ "links": [
+ {
+ "link_doctype": "BOM",
+ "link_fieldname": "bom_creator"
+ }
+ ],
+ "modified": "2023-08-07 15:45:06.176313",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "BOM Creator",
+ "naming_rule": "Set by user",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Manufacturing Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Manufacturing User",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ }
+ ],
+ "show_name_in_global_search": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
new file mode 100644
index 0000000..999d610
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
@@ -0,0 +1,424 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from collections import OrderedDict
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils import flt
+
+from erpnext.manufacturing.doctype.bom.bom import get_bom_item_rate
+
+BOM_FIELDS = [
+ "company",
+ "rm_cost_as_per",
+ "project",
+ "currency",
+ "conversion_rate",
+ "buying_price_list",
+]
+
+BOM_ITEM_FIELDS = [
+ "item_code",
+ "qty",
+ "uom",
+ "rate",
+ "stock_qty",
+ "stock_uom",
+ "conversion_factor",
+ "do_not_explode",
+]
+
+
+class BOMCreator(Document):
+ def before_save(self):
+ self.set_status()
+ self.set_is_expandable()
+ self.set_conversion_factor()
+ self.set_reference_id()
+ self.set_rate_for_items()
+
+ def validate(self):
+ self.validate_items()
+
+ def validate_items(self):
+ for row in self.items:
+ if row.is_expandable and row.item_code == self.item_code:
+ frappe.throw(_("Item {0} cannot be added as a sub-assembly of itself").format(row.item_code))
+
+ def set_status(self, save=False):
+ self.status = {
+ 0: "Draft",
+ 1: "Submitted",
+ 2: "Cancelled",
+ }[self.docstatus]
+
+ self.set_status_completed()
+ if save:
+ self.db_set("status", self.status)
+
+ def set_status_completed(self):
+ if self.docstatus != 1:
+ return
+
+ has_completed = True
+ for row in self.items:
+ if row.is_expandable and not row.bom_created:
+ has_completed = False
+ break
+
+ if not frappe.get_cached_value(
+ "BOM", {"bom_creator": self.name, "item": self.item_code}, "name"
+ ):
+ has_completed = False
+
+ if has_completed:
+ self.status = "Completed"
+
+ def on_cancel(self):
+ self.set_status(True)
+
+ def set_conversion_factor(self):
+ for row in self.items:
+ row.conversion_factor = 1.0
+
+ def before_submit(self):
+ self.validate_fields()
+ self.set_status()
+
+ def set_reference_id(self):
+ parent_reference = {row.idx: row.name for row in self.items}
+
+ for row in self.items:
+ if row.fg_reference_id:
+ continue
+
+ if row.parent_row_no:
+ row.fg_reference_id = parent_reference.get(row.parent_row_no)
+
+ @frappe.whitelist()
+ def add_boms(self):
+ self.submit()
+
+ def set_rate_for_items(self):
+ if self.rm_cost_as_per == "Manual":
+ return
+
+ amount = self.get_raw_material_cost()
+ self.raw_material_cost = amount
+
+ def get_raw_material_cost(self, fg_reference_id=None, amount=0):
+ if not fg_reference_id:
+ fg_reference_id = self.name
+
+ for row in self.items:
+ if row.fg_reference_id != fg_reference_id:
+ continue
+
+ if not row.is_expandable:
+ row.rate = get_bom_item_rate(
+ {
+ "company": self.company,
+ "item_code": row.item_code,
+ "bom_no": "",
+ "qty": row.qty,
+ "uom": row.uom,
+ "stock_uom": row.stock_uom,
+ "conversion_factor": row.conversion_factor,
+ "sourced_by_supplier": row.sourced_by_supplier,
+ },
+ self,
+ )
+
+ row.amount = flt(row.rate) * flt(row.qty)
+
+ else:
+ row.amount = 0.0
+ row.amount = self.get_raw_material_cost(row.name, row.amount)
+ row.rate = flt(row.amount) / (flt(row.qty) * flt(row.conversion_factor))
+
+ amount += flt(row.amount)
+
+ return amount
+
+ def set_is_expandable(self):
+ fg_items = [row.fg_item for row in self.items if row.fg_item != self.item_code]
+ for row in self.items:
+ row.is_expandable = 0
+ if row.item_code in fg_items:
+ row.is_expandable = 1
+
+ def validate_fields(self):
+ fields = {
+ "items": "Items",
+ }
+
+ for field, label in fields.items():
+ if not self.get(field):
+ frappe.throw(_("Please set {0} in BOM Creator {1}").format(label, self.name))
+
+ def on_submit(self):
+ self.enqueue_create_boms()
+
+ def enqueue_create_boms(self):
+ frappe.enqueue(
+ self.create_boms,
+ queue="short",
+ timeout=600,
+ is_async=True,
+ )
+
+ frappe.msgprint(
+ _("BOMs creation has been enqueued, kindly check the status after some time"), alert=True
+ )
+
+ def create_boms(self):
+ """
+ Sample data structure of production_item_wise_rm
+ production_item_wise_rm = {
+ (fg_item_code, name): {
+ "items": [],
+ "bom_no": "",
+ "fg_item_data": {}
+ }
+ }
+ """
+
+ self.db_set("status", "In Progress")
+ production_item_wise_rm = OrderedDict({})
+ production_item_wise_rm.setdefault(
+ (self.item_code, self.name), frappe._dict({"items": [], "bom_no": "", "fg_item_data": self})
+ )
+
+ for row in self.items:
+ if row.is_expandable:
+ if (row.item_code, row.name) not in production_item_wise_rm:
+ production_item_wise_rm.setdefault(
+ (row.item_code, row.name), frappe._dict({"items": [], "bom_no": "", "fg_item_data": row})
+ )
+
+ production_item_wise_rm[(row.fg_item, row.fg_reference_id)]["items"].append(row)
+
+ reverse_tree = OrderedDict(reversed(list(production_item_wise_rm.items())))
+
+ try:
+ for d in reverse_tree:
+ fg_item_data = production_item_wise_rm.get(d).fg_item_data
+ self.create_bom(fg_item_data, production_item_wise_rm)
+
+ frappe.msgprint(_("BOMs created successfully"))
+ except Exception:
+ traceback = frappe.get_traceback()
+ self.db_set(
+ {
+ "status": "Failed",
+ "error_log": traceback,
+ }
+ )
+
+ frappe.msgprint(_("BOMs creation failed"))
+
+ def create_bom(self, row, production_item_wise_rm):
+ bom = frappe.new_doc("BOM")
+ bom.update(
+ {
+ "item": row.item_code,
+ "bom_type": "Production",
+ "quantity": row.qty,
+ "allow_alternative_item": 1,
+ "bom_creator": self.name,
+ "bom_creator_item": row.name if row.name != self.name else "",
+ "rm_cost_as_per": "Manual",
+ }
+ )
+
+ for field in BOM_FIELDS:
+ if self.get(field):
+ bom.set(field, self.get(field))
+
+ for item in production_item_wise_rm[(row.item_code, row.name)]["items"]:
+ bom_no = ""
+ item.do_not_explode = 1
+ if (item.item_code, item.name) in production_item_wise_rm:
+ bom_no = production_item_wise_rm.get((item.item_code, item.name)).bom_no
+ item.do_not_explode = 0
+
+ item_args = {}
+ for field in BOM_ITEM_FIELDS:
+ item_args[field] = item.get(field)
+
+ item_args.update(
+ {
+ "bom_no": bom_no,
+ "allow_alternative_item": 1,
+ "allow_scrap_items": 1,
+ "include_item_in_manufacturing": 1,
+ }
+ )
+
+ bom.append("items", item_args)
+
+ bom.save(ignore_permissions=True)
+ bom.submit()
+
+ production_item_wise_rm[(row.item_code, row.name)].bom_no = bom.name
+
+ @frappe.whitelist()
+ def get_default_bom(self, item_code) -> str:
+ return frappe.get_cached_value("Item", item_code, "default_bom")
+
+
+@frappe.whitelist()
+def get_children(doctype=None, parent=None, **kwargs):
+ if isinstance(kwargs, str):
+ kwargs = frappe.parse_json(kwargs)
+
+ if isinstance(kwargs, dict):
+ kwargs = frappe._dict(kwargs)
+
+ fields = [
+ "item_code as value",
+ "is_expandable as expandable",
+ "parent as parent_id",
+ "qty",
+ "idx",
+ "'BOM Creator Item' as doctype",
+ "name",
+ "uom",
+ "rate",
+ "amount",
+ ]
+
+ query_filters = {
+ "fg_item": parent,
+ "parent": kwargs.parent_id,
+ }
+
+ if kwargs.name:
+ query_filters["name"] = kwargs.name
+
+ return frappe.get_all("BOM Creator Item", fields=fields, filters=query_filters, order_by="idx")
+
+
+@frappe.whitelist()
+def add_item(**kwargs):
+ if isinstance(kwargs, str):
+ kwargs = frappe.parse_json(kwargs)
+
+ if isinstance(kwargs, dict):
+ kwargs = frappe._dict(kwargs)
+
+ doc = frappe.get_doc("BOM Creator", kwargs.parent)
+ item_info = get_item_details(kwargs.item_code)
+ kwargs.update(
+ {
+ "uom": item_info.stock_uom,
+ "stock_uom": item_info.stock_uom,
+ "conversion_factor": 1,
+ }
+ )
+
+ doc.append("items", kwargs)
+ doc.save()
+
+ return doc
+
+
+@frappe.whitelist()
+def add_sub_assembly(**kwargs):
+ if isinstance(kwargs, str):
+ kwargs = frappe.parse_json(kwargs)
+
+ if isinstance(kwargs, dict):
+ kwargs = frappe._dict(kwargs)
+
+ doc = frappe.get_doc("BOM Creator", kwargs.parent)
+ bom_item = frappe.parse_json(kwargs.bom_item)
+
+ name = kwargs.fg_reference_id
+ parent_row_no = ""
+ if not kwargs.convert_to_sub_assembly:
+ item_info = get_item_details(bom_item.item_code)
+ item_row = doc.append(
+ "items",
+ {
+ "item_code": bom_item.item_code,
+ "qty": bom_item.qty,
+ "uom": item_info.stock_uom,
+ "fg_item": kwargs.fg_item,
+ "conversion_factor": 1,
+ "fg_reference_id": name,
+ "stock_qty": bom_item.qty,
+ "fg_reference_id": name,
+ "do_not_explode": 1,
+ "is_expandable": 1,
+ "stock_uom": item_info.stock_uom,
+ },
+ )
+
+ parent_row_no = item_row.idx
+ name = ""
+
+ for row in bom_item.get("items"):
+ row = frappe._dict(row)
+ item_info = get_item_details(row.item_code)
+ doc.append(
+ "items",
+ {
+ "item_code": row.item_code,
+ "qty": row.qty,
+ "fg_item": bom_item.item_code,
+ "uom": item_info.stock_uom,
+ "fg_reference_id": name,
+ "parent_row_no": parent_row_no,
+ "conversion_factor": 1,
+ "do_not_explode": 1,
+ "stock_qty": row.qty,
+ "stock_uom": item_info.stock_uom,
+ },
+ )
+
+ doc.save()
+
+ return doc
+
+
+def get_item_details(item_code):
+ return frappe.get_cached_value(
+ "Item", item_code, ["item_name", "description", "image", "stock_uom", "default_bom"], as_dict=1
+ )
+
+
+@frappe.whitelist()
+def delete_node(**kwargs):
+ if isinstance(kwargs, str):
+ kwargs = frappe.parse_json(kwargs)
+
+ if isinstance(kwargs, dict):
+ kwargs = frappe._dict(kwargs)
+
+ items = get_children(parent=kwargs.fg_item, parent_id=kwargs.parent)
+ if kwargs.docname:
+ frappe.delete_doc("BOM Creator Item", kwargs.docname)
+
+ for item in items:
+ frappe.delete_doc("BOM Creator Item", item.name)
+ if item.expandable:
+ delete_node(fg_item=item.value, parent=item.parent_id)
+
+ doc = frappe.get_doc("BOM Creator", kwargs.parent)
+ doc.set_rate_for_items()
+ doc.save()
+
+ return doc
+
+
+@frappe.whitelist()
+def edit_qty(doctype, docname, qty, parent):
+ frappe.db.set_value(doctype, docname, "qty", qty)
+ doc = frappe.get_doc("BOM Creator", parent)
+ doc.set_rate_for_items()
+ doc.save()
+
+ return doc
diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator_list.js b/erpnext/manufacturing/doctype/bom_creator/bom_creator_list.js
new file mode 100644
index 0000000..423b721
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator_list.js
@@ -0,0 +1,18 @@
+frappe.listview_settings['BOM Creator'] = {
+ add_fields: ["status"],
+ get_indicator: function (doc) {
+ if (doc.status === "Draft") {
+ return [__("Draft"), "red", "status,=,Draft"];
+ } else if (doc.status === "In Progress") {
+ return [__("In Progress"), "orange", "status,=,In Progress"];
+ } else if (doc.status === "Completed") {
+ return [__("Completed"), "green", "status,=,Completed"];
+ } else if (doc.status === "Cancelled") {
+ return [__("Cancelled"), "red", "status,=,Cancelled"];
+ } else if (doc.status === "Failed") {
+ return [__("Failed"), "red", "status,=,Failed"];
+ } else if (doc.status === "Submitted") {
+ return [__("Submitted"), "blue", "status,=,Submitted"];
+ }
+ },
+};
diff --git a/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py
new file mode 100644
index 0000000..d239d58
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py
@@ -0,0 +1,240 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import random
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+
+from erpnext.manufacturing.doctype.bom_creator.bom_creator import (
+ add_item,
+ add_sub_assembly,
+ delete_node,
+ edit_qty,
+)
+from erpnext.stock.doctype.item.test_item import make_item
+
+
+class TestBOMCreator(FrappeTestCase):
+ def setUp(self) -> None:
+ create_items()
+
+ def test_bom_sub_assembly(self):
+ final_product = "Bicycle"
+ make_item(
+ final_product,
+ {
+ "item_group": "Raw Material",
+ "stock_uom": "Nos",
+ },
+ )
+
+ doc = make_bom_creator(
+ name="Bicycle BOM with Sub Assembly",
+ company="_Test Company",
+ item_code=final_product,
+ qty=1,
+ rm_cosy_as_per="Valuation Rate",
+ currency="INR",
+ plc_conversion_rate=1,
+ conversion_rate=1,
+ )
+
+ add_sub_assembly(
+ parent=doc.name,
+ fg_item=final_product,
+ fg_reference_id=doc.name,
+ bom_item={
+ "item_code": "Frame Assembly",
+ "qty": 1,
+ "items": [
+ {
+ "item_code": "Frame",
+ "qty": 1,
+ },
+ {
+ "item_code": "Fork",
+ "qty": 1,
+ },
+ ],
+ },
+ )
+
+ doc.reload()
+ self.assertEqual(doc.items[0].item_code, "Frame Assembly")
+
+ fg_valuation_rate = 0
+ for row in doc.items:
+ if not row.is_expandable:
+ fg_valuation_rate += row.amount
+ self.assertEqual(row.fg_item, "Frame Assembly")
+ self.assertEqual(row.fg_reference_id, doc.items[0].name)
+
+ self.assertEqual(doc.items[0].amount, fg_valuation_rate)
+
+ def test_bom_raw_material(self):
+ final_product = "Bicycle"
+ make_item(
+ final_product,
+ {
+ "item_group": "Raw Material",
+ "stock_uom": "Nos",
+ },
+ )
+
+ doc = make_bom_creator(
+ name="Bicycle BOM with Raw Material",
+ company="_Test Company",
+ item_code=final_product,
+ qty=1,
+ rm_cosy_as_per="Valuation Rate",
+ currency="INR",
+ plc_conversion_rate=1,
+ conversion_rate=1,
+ )
+
+ add_item(
+ parent=doc.name,
+ fg_item=final_product,
+ fg_reference_id=doc.name,
+ item_code="Pedal Assembly",
+ qty=2,
+ )
+
+ doc.reload()
+ self.assertEqual(doc.items[0].item_code, "Pedal Assembly")
+ self.assertEqual(doc.items[0].qty, 2)
+
+ fg_valuation_rate = 0
+ for row in doc.items:
+ if not row.is_expandable:
+ fg_valuation_rate += row.amount
+ self.assertEqual(row.fg_item, "Bicycle")
+ self.assertEqual(row.fg_reference_id, doc.name)
+
+ self.assertEqual(doc.raw_material_cost, fg_valuation_rate)
+
+ def test_convert_to_sub_assembly(self):
+ final_product = "Bicycle"
+ make_item(
+ final_product,
+ {
+ "item_group": "Raw Material",
+ "stock_uom": "Nos",
+ },
+ )
+
+ doc = make_bom_creator(
+ name="Bicycle BOM",
+ company="_Test Company",
+ item_code=final_product,
+ qty=1,
+ rm_cosy_as_per="Valuation Rate",
+ currency="INR",
+ plc_conversion_rate=1,
+ conversion_rate=1,
+ )
+
+ add_item(
+ parent=doc.name,
+ fg_item=final_product,
+ fg_reference_id=doc.name,
+ item_code="Pedal Assembly",
+ qty=2,
+ )
+
+ doc.reload()
+ self.assertEqual(doc.items[0].is_expandable, 0)
+
+ add_sub_assembly(
+ convert_to_sub_assembly=1,
+ parent=doc.name,
+ fg_item=final_product,
+ fg_reference_id=doc.items[0].name,
+ bom_item={
+ "item_code": "Pedal Assembly",
+ "qty": 2,
+ "items": [
+ {
+ "item_code": "Pedal Body",
+ "qty": 2,
+ },
+ {
+ "item_code": "Pedal Axle",
+ "qty": 2,
+ },
+ ],
+ },
+ )
+
+ doc.reload()
+ self.assertEqual(doc.items[0].is_expandable, 1)
+
+ fg_valuation_rate = 0
+ for row in doc.items:
+ if not row.is_expandable:
+ fg_valuation_rate += row.amount
+ self.assertEqual(row.fg_item, "Pedal Assembly")
+ self.assertEqual(row.qty, 2.0)
+ self.assertEqual(row.fg_reference_id, doc.items[0].name)
+
+ self.assertEqual(doc.raw_material_cost, fg_valuation_rate)
+
+
+def create_items():
+ raw_materials = [
+ "Frame",
+ "Fork",
+ "Rim",
+ "Spokes",
+ "Hub",
+ "Tube",
+ "Tire",
+ "Pedal Body",
+ "Pedal Axle",
+ "Ball Bearings",
+ "Chain Links",
+ "Chain Pins",
+ "Seat",
+ "Seat Post",
+ "Seat Clamp",
+ ]
+
+ for item in raw_materials:
+ valuation_rate = random.choice([100, 200, 300, 500, 333, 222, 44, 20, 10])
+ make_item(
+ item,
+ {
+ "item_group": "Raw Material",
+ "stock_uom": "Nos",
+ "valuation_rate": valuation_rate,
+ },
+ )
+
+ sub_assemblies = [
+ "Frame Assembly",
+ "Wheel Assembly",
+ "Pedal Assembly",
+ "Chain Assembly",
+ "Seat Assembly",
+ ]
+
+ for item in sub_assemblies:
+ make_item(
+ item,
+ {
+ "item_group": "Raw Material",
+ "stock_uom": "Nos",
+ },
+ )
+
+
+def make_bom_creator(**kwargs):
+ if isinstance(kwargs, str) or isinstance(kwargs, dict):
+ kwargs = frappe.parse_json(kwargs)
+
+ doc = frappe.new_doc("BOM Creator")
+ doc.update(kwargs)
+ doc.save()
+
+ return doc
diff --git a/erpnext/manufacturing/doctype/bom_creator_item/__init__.py b/erpnext/manufacturing/doctype/bom_creator_item/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_creator_item/__init__.py
diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json
new file mode 100644
index 0000000..fdb5d3a
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json
@@ -0,0 +1,243 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2023-07-18 14:35:50.307386",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "item_code",
+ "item_name",
+ "item_group",
+ "column_break_f63f",
+ "fg_item",
+ "source_warehouse",
+ "is_expandable",
+ "sourced_by_supplier",
+ "bom_created",
+ "description_section",
+ "description",
+ "quantity_and_rate_section",
+ "qty",
+ "rate",
+ "uom",
+ "column_break_bgnb",
+ "stock_qty",
+ "conversion_factor",
+ "stock_uom",
+ "amount_section",
+ "amount",
+ "column_break_yuca",
+ "base_rate",
+ "base_amount",
+ "section_break_wtld",
+ "do_not_explode",
+ "parent_row_no",
+ "fg_reference_id",
+ "column_break_sulm",
+ "instruction"
+ ],
+ "fields": [
+ {
+ "columns": 2,
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Item Code",
+ "options": "Item",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "item_code.item_name",
+ "fetch_if_empty": 1,
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "label": "Item Name"
+ },
+ {
+ "fetch_from": "item_code.item_group",
+ "fieldname": "item_group",
+ "fieldtype": "Link",
+ "label": "Item Group",
+ "options": "Item Group"
+ },
+ {
+ "fieldname": "column_break_f63f",
+ "fieldtype": "Column Break"
+ },
+ {
+ "columns": 2,
+ "fieldname": "fg_item",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "FG Item",
+ "options": "Item",
+ "reqd": 1
+ },
+ {
+ "fieldname": "source_warehouse",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Source Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_expandable",
+ "fieldtype": "Check",
+ "label": "Is Expandable",
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "description_section",
+ "fieldtype": "Section Break",
+ "label": "Description"
+ },
+ {
+ "fetch_from": "item_code.description",
+ "fetch_if_empty": 1,
+ "fieldname": "description",
+ "fieldtype": "Small Text"
+ },
+ {
+ "fieldname": "quantity_and_rate_section",
+ "fieldtype": "Section Break",
+ "label": "Quantity and Rate"
+ },
+ {
+ "columns": 1,
+ "fieldname": "qty",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Qty"
+ },
+ {
+ "columns": 2,
+ "fieldname": "rate",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Rate"
+ },
+ {
+ "columns": 1,
+ "fieldname": "uom",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "UOM",
+ "options": "UOM"
+ },
+ {
+ "fieldname": "column_break_bgnb",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "stock_qty",
+ "fieldtype": "Float",
+ "label": "Stock Qty",
+ "read_only": 1
+ },
+ {
+ "fieldname": "conversion_factor",
+ "fieldtype": "Float",
+ "label": "Conversion Factor"
+ },
+ {
+ "fetch_from": "item_code.stock_uom",
+ "fieldname": "stock_uom",
+ "fieldtype": "Link",
+ "label": "Stock UOM",
+ "no_copy": 1,
+ "options": "UOM",
+ "read_only": 1
+ },
+ {
+ "fieldname": "amount_section",
+ "fieldtype": "Section Break",
+ "label": "Amount"
+ },
+ {
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "label": "Amount",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_yuca",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "1",
+ "fieldname": "do_not_explode",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Do Not Explode"
+ },
+ {
+ "fieldname": "instruction",
+ "fieldtype": "Small Text",
+ "label": "Instruction"
+ },
+ {
+ "fieldname": "base_amount",
+ "fieldtype": "Currency",
+ "hidden": 1,
+ "label": "Base Amount"
+ },
+ {
+ "fieldname": "base_rate",
+ "fieldtype": "Currency",
+ "hidden": 1,
+ "label": "Base Rate"
+ },
+ {
+ "default": "0",
+ "fieldname": "sourced_by_supplier",
+ "fieldtype": "Check",
+ "label": "Sourced by Supplier"
+ },
+ {
+ "fieldname": "section_break_wtld",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "fg_reference_id",
+ "fieldtype": "Data",
+ "label": "FG Reference",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_sulm",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "parent_row_no",
+ "fieldtype": "Data",
+ "label": "Parent Row No",
+ "no_copy": 1,
+ "print_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "bom_created",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "BOM Created",
+ "no_copy": 1,
+ "print_hide": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2023-08-07 11:52:30.492233",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "BOM Creator Item",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py
new file mode 100644
index 0000000..350c918
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class BOMCreatorItem(Document):
+ pass
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 261aa76..131f438 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -347,7 +347,7 @@
if not data.pending_qty:
continue
- item_details = get_item_details(data.item_code)
+ item_details = get_item_details(data.item_code, throw=False)
if self.combine_items:
if item_details.bom_no in refs:
refs[item_details.bom_no]["so_details"].append(
@@ -795,6 +795,9 @@
if not row.item_code:
frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx))
+ if not row.bom_no:
+ frappe.throw(_("Row #{0}: Please select the BOM No in Assembly Items").format(row.idx))
+
bom_data = []
warehouse = row.warehouse if self.skip_available_sub_assembly_item else None
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json
index a236f2a..1996e19 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.json
+++ b/erpnext/manufacturing/doctype/work_order/work_order.json
@@ -404,6 +404,8 @@
"read_only": 1
},
{
+ "fetch_from": "production_item.stock_uom",
+ "fetch_if_empty": 1,
"fieldname": "stock_uom",
"fieldtype": "Link",
"label": "Stock UOM",
@@ -590,7 +592,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2023-06-09 13:20:09.154362",
+ "modified": "2023-08-11 18:35:49.852069",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order",
@@ -610,7 +612,6 @@
"read": 1,
"report": 1,
"role": "Manufacturing User",
- "set_user_permissions": 1,
"share": 1,
"submit": 1,
"write": 1
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 7c15bf9..5ad79f9 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -1082,7 +1082,7 @@
@frappe.whitelist()
-def get_item_details(item, project=None, skip_bom_info=False):
+def get_item_details(item, project=None, skip_bom_info=False, throw=True):
res = frappe.db.sql(
"""
select stock_uom, description, item_name, allow_alternative_item,
@@ -1118,12 +1118,15 @@
if not res["bom_no"]:
if project:
- res = get_item_details(item)
+ res = get_item_details(item, throw=throw)
frappe.msgprint(
_("Default BOM not found for Item {0} and Project {1}").format(item, project), alert=1
)
else:
- frappe.throw(_("Default BOM for {0} not found").format(item))
+ msg = _("Default BOM for {0} not found").format(item)
+ frappe.msgprint(msg, raise_exception=throw, indicator="yellow", alert=(not throw))
+
+ return res
bom_data = frappe.db.get_value(
"BOM",
diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
index 518ae14..8e07850 100644
--- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
+++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
@@ -1,6 +1,6 @@
{
"charts": [],
- "content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"PIQJYZOMnD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Manufacturing\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
+ "content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"YHCQG3wAGv\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Creator\",\"col\":3}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"PIQJYZOMnD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Manufacturing\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
"creation": "2020-03-02 17:11:37.032604",
"custom_blocks": [],
"docstatus": 0,
@@ -316,7 +316,7 @@
"type": "Link"
}
],
- "modified": "2023-07-04 14:40:47.281125",
+ "modified": "2023-08-08 22:28:39.633891",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing",
@@ -339,6 +339,13 @@
{
"color": "Grey",
"doc_view": "List",
+ "label": "BOM Creator",
+ "link_to": "BOM Creator",
+ "type": "DocType"
+ },
+ {
+ "color": "Grey",
+ "doc_view": "List",
"label": "BOM",
"link_to": "BOM",
"stats_filter": "{\"is_active\":[\"=\",1]}",
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index d035ad6..a25c7c2 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -322,8 +322,6 @@
execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
erpnext.patches.v14_0.update_reference_type_in_journal_entry_accounts
erpnext.patches.v14_0.update_subscription_details
-# below migration patches should always run last
-erpnext.patches.v14_0.migrate_gl_to_payment_ledger
execute:frappe.delete_doc_if_exists("Report", "Tax Detail")
erpnext.patches.v15_0.enable_all_leads
erpnext.patches.v14_0.update_company_in_ldc
@@ -340,3 +338,6 @@
execute:frappe.defaults.clear_default("fiscal_year")
erpnext.patches.v15_0.remove_exotel_integration
erpnext.patches.v14_0.single_to_multi_dunning
+execute:frappe.db.set_single_value('Selling Settings', 'allow_negative_rates_for_items', 0)
+# below migration patch should always run last
+erpnext.patches.v14_0.migrate_gl_to_payment_ledger
diff --git a/erpnext/projects/report/billing_summary.py b/erpnext/projects/report/billing_summary.py
index bc8f2af..ac1524a 100644
--- a/erpnext/projects/report/billing_summary.py
+++ b/erpnext/projects/report/billing_summary.py
@@ -98,9 +98,11 @@
record_filters = [
["start_date", "<=", filters.to_date],
["end_date", ">=", filters.from_date],
- ["docstatus", "=", 1],
]
-
+ if not filters.get("include_draft_timesheets"):
+ record_filters.append(["docstatus", "=", 1])
+ else:
+ record_filters.append(["docstatus", "!=", 2])
if "employee" in filters:
record_filters.append(["employee", "=", filters.employee])
diff --git a/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js b/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js
index 8566b1f..2c25465 100644
--- a/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js
+++ b/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js
@@ -25,5 +25,10 @@
default: frappe.datetime.add_days(frappe.datetime.month_start(), -1),
reqd: 1
},
+ {
+ fieldname:"include_draft_timesheets",
+ label: __("Include Timesheets in Draft Status"),
+ fieldtype: "Check",
+ },
]
}
diff --git a/erpnext/projects/report/project_billing_summary/project_billing_summary.js b/erpnext/projects/report/project_billing_summary/project_billing_summary.js
index 0242036..fce0c68 100644
--- a/erpnext/projects/report/project_billing_summary/project_billing_summary.js
+++ b/erpnext/projects/report/project_billing_summary/project_billing_summary.js
@@ -25,5 +25,10 @@
default: frappe.datetime.add_days(frappe.datetime.month_start(),-1),
reqd: 1
},
+ {
+ fieldname:"include_draft_timesheets",
+ label: __("Include Timesheets in Draft Status"),
+ fieldtype: "Check",
+ },
]
}
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
deleted file mode 100644
index b9b48ab..0000000
--- a/erpnext/public/build.json
+++ /dev/null
@@ -1,68 +0,0 @@
-{
- "css/erpnext.css": [
- "public/less/erpnext.less",
- "public/scss/call_popup.scss",
- "public/scss/point-of-sale.scss"
- ],
- "js/erpnext-web.min.js": [
- "public/js/website_utils.js",
- "public/js/shopping_cart.js",
- "public/js/wishlist.js"
- ],
- "css/erpnext-web.css": [
- "public/scss/website.scss",
- "public/scss/shopping_cart.scss"
- ],
- "js/erpnext.min.js": [
- "public/js/conf.js",
- "public/js/utils.js",
- "public/js/queries.js",
- "public/js/sms_manager.js",
- "public/js/utils/party.js",
- "public/js/controllers/stock_controller.js",
- "public/js/payment/payments.js",
- "public/js/controllers/taxes_and_totals.js",
- "public/js/controllers/transaction.js",
- "public/js/templates/item_selector.html",
- "public/js/utils/item_selector.js",
- "public/js/help_links.js",
- "public/js/templates/item_quick_entry.html",
- "public/js/utils/customer_quick_entry.js",
- "public/js/utils/supplier_quick_entry.js",
- "public/js/education/student_button.html",
- "public/js/education/assessment_result_tool.html",
- "public/js/call_popup/call_popup.js",
- "public/js/utils/dimension_tree_filter.js",
- "public/js/telephony.js",
- "public/js/templates/call_link.html",
- "public/js/bulk_transaction_processing.js"
- ],
- "js/item-dashboard.min.js": [
- "stock/dashboard/item_dashboard.html",
- "stock/dashboard/item_dashboard_list.html",
- "stock/dashboard/item_dashboard.js",
- "stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html",
- "stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html"
- ],
- "js/point-of-sale.min.js": [
- "selling/page/point_of_sale/pos_item_selector.js",
- "selling/page/point_of_sale/pos_item_cart.js",
- "selling/page/point_of_sale/pos_item_details.js",
- "selling/page/point_of_sale/pos_number_pad.js",
- "selling/page/point_of_sale/pos_payment.js",
- "selling/page/point_of_sale/pos_past_order_list.js",
- "selling/page/point_of_sale/pos_past_order_summary.js",
- "selling/page/point_of_sale/pos_controller.js"
- ],
- "js/bank-reconciliation-tool.min.js": [
- "public/js/bank_reconciliation_tool/data_table_manager.js",
- "public/js/bank_reconciliation_tool/number_card.js",
- "public/js/bank_reconciliation_tool/dialog_manager.js"
- ],
- "js/e-commerce.min.js": [
- "e_commerce/product_ui/views.js",
- "e_commerce/product_ui/grid.js",
- "e_commerce/product_ui/list.js",
- "e_commerce/product_ui/search.js"
- ]
-}
diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
index cbb64ca..52fa8ab 100644
--- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
+++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
@@ -104,6 +104,9 @@
name: __("Document Name"),
editable: false,
width: 1,
+ format: (value, row) => {
+ return frappe.form.formatters.Link(value, {options: row[2].content});
+ },
},
{
name: __("Reference Date"),
@@ -132,7 +135,7 @@
format_row(row) {
return [
row[1], // Document Type
- frappe.form.formatters.Link(row[2], {options: row[1]}), // Document Name
+ row[2], // Document Name
row[5] || row[8], // Reference Date
format_currency(row[3], row[9]), // Remaining
row[4], // Reference Number
diff --git a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js
new file mode 100644
index 0000000..b3b2e9f
--- /dev/null
+++ b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js
@@ -0,0 +1,416 @@
+class BOMConfigurator {
+ constructor({ wrapper, page, frm, bom_configurator }) {
+ this.$wrapper = $(wrapper);
+ this.page = page;
+ this.bom_configurator = bom_configurator;
+ this.frm = frm;
+
+ this.make();
+ this.prepare_layout();
+ this.bind_events();
+ }
+
+ add_boms() {
+ this.frm.call({
+ method: "add_boms",
+ freeze: true,
+ doc: this.frm.doc,
+ });
+ }
+
+ make() {
+ let options = {
+ ...this.tree_options(),
+ ...this.tree_methods(),
+ };
+
+ frappe.views.trees["BOM Configurator"] = new frappe.views.TreeView(options);
+ this.tree_view = frappe.views.trees["BOM Configurator"];
+ }
+
+ bind_events() {
+ frappe.views.trees["BOM Configurator"].events = {
+ frm: this.frm,
+ add_item: this.add_item,
+ add_sub_assembly: this.add_sub_assembly,
+ get_sub_assembly_modal_fields: this.get_sub_assembly_modal_fields,
+ convert_to_sub_assembly: this.convert_to_sub_assembly,
+ delete_node: this.delete_node,
+ edit_qty: this.edit_qty,
+ load_tree: this.load_tree,
+ set_default_qty: this.set_default_qty,
+ }
+ }
+
+ tree_options() {
+ return {
+ parent: this.$wrapper.get(0),
+ body: this.$wrapper.get(0),
+ doctype: 'BOM Configurator',
+ page: this.page,
+ expandable: true,
+ title: __("Configure Product Assembly"),
+ breadcrumb: "Manufacturing",
+ get_tree_nodes: "erpnext.manufacturing.doctype.bom_creator.bom_creator.get_children",
+ root_label: this.frm.doc.item_code,
+ disable_add_node: true,
+ get_tree_root: false,
+ show_expand_all: false,
+ extend_toolbar: false,
+ do_not_make_page: true,
+ do_not_setup_menu: true,
+ }
+ }
+
+ tree_methods() {
+ let frm_obj = this;
+ let view = frappe.views.trees["BOM Configurator"];
+
+ return {
+ onload: function(me) {
+ me.args["parent_id"] = frm_obj.frm.doc.name;
+ me.args["parent"] = frm_obj.frm.doc.item_code;
+ me.parent = frm_obj.$wrapper.get(0);
+ me.body = frm_obj.$wrapper.get(0);
+ me.make_tree();
+ },
+ onrender(node) {
+ const qty = node.data.qty || frm_obj.frm.doc.qty;
+ const uom = node.data.uom || frm_obj.frm.doc.uom;
+ const docname = node.data.name || frm_obj.frm.doc.name;
+ let amount = node.data.amount;
+ if (node.data.value === frm_obj.frm.doc.item_code) {
+ amount = frm_obj.frm.doc.raw_material_cost;
+ }
+
+ amount = frappe.format(amount, { fieldtype: "Currency", currency: frm_obj.frm.doc.currency });
+
+ $(`
+ <div class="pill small pull-right bom-qty-pill"
+ style="background-color: var(--bg-white);
+ color: var(--text-on-gray);
+ font-weight:450;
+ margin-right: 40px;
+ display: inline-flex;
+ min-width: 128px;
+ border: 1px solid var(--bg-gray);
+ ">
+ <div style="padding-right:5px" data-bom-qty-docname="${docname}">${qty} ${uom}</div>
+ <div class="fg-item-amt" style="padding-left:12px; border-left:1px solid var(--bg-gray)">
+ ${amount}
+ </div>
+ </div>
+
+ `).insertBefore(node.$ul);
+ },
+ toolbar: this.frm?.doc.docstatus === 0 ? [
+ {
+ label:__(frappe.utils.icon('edit', 'sm') + " Qty"),
+ click: function(node) {
+ let view = frappe.views.trees["BOM Configurator"];
+ view.events.edit_qty(node, view);
+ },
+ btnClass: "hidden-xs"
+ },
+ {
+ label:__(frappe.utils.icon('add', 'sm') + " Raw Material"),
+ click: function(node) {
+ let view = frappe.views.trees["BOM Configurator"];
+ view.events.add_item(node, view);
+ },
+ condition: function(node) {
+ return node.expandable;
+ },
+ btnClass: "hidden-xs"
+ },
+ {
+ label:__(frappe.utils.icon('add', 'sm') + " Sub Assembly"),
+ click: function(node) {
+ let view = frappe.views.trees["BOM Configurator"];
+ view.events.add_sub_assembly(node, view);
+ },
+ condition: function(node) {
+ return node.expandable;
+ },
+ btnClass: "hidden-xs"
+ },
+ {
+ label:__("Expand All"),
+ click: function(node) {
+ let view = frappe.views.trees["BOM Configurator"];
+
+ if (!node.expanded) {
+ view.tree.load_children(node, true);
+ $(node.parent[0]).find(".tree-children").show();
+ node.$toolbar.find(".expand-all-btn").html("Collapse All");
+ } else {
+ node.$tree_link.trigger("click");
+ node.$toolbar.find(".expand-all-btn").html("Expand All");
+ }
+ },
+ condition: function(node) {
+ return node.expandable && node.is_root;
+ },
+ btnClass: "hidden-xs expand-all-btn"
+ },
+ {
+ label:__(frappe.utils.icon('move', 'sm') + " Sub Assembly"),
+ click: function(node) {
+ let view = frappe.views.trees["BOM Configurator"];
+ view.events.convert_to_sub_assembly(node, view);
+ },
+ condition: function(node) {
+ return !node.expandable;
+ },
+ btnClass: "hidden-xs"
+ },
+ {
+ label:__(frappe.utils.icon('delete', 'sm') + __(" Item")),
+ click: function(node) {
+ let view = frappe.views.trees["BOM Configurator"];
+ view.events.delete_node(node, view);
+ },
+ condition: function(node) {
+ return !node.is_root;
+ },
+ btnClass: "hidden-xs"
+ },
+ ] : [{
+ label:__("Expand All"),
+ click: function(node) {
+ let view = frappe.views.trees["BOM Configurator"];
+
+ if (!node.expanded) {
+ view.tree.load_children(node, true);
+ $(node.parent[0]).find(".tree-children").show();
+ node.$toolbar.find(".expand-all-btn").html("Collapse All");
+ } else {
+ node.$tree_link.trigger("click");
+ node.$toolbar.find(".expand-all-btn").html("Expand All");
+ }
+ },
+ condition: function(node) {
+ return node.expandable && node.is_root;
+ },
+ btnClass: "hidden-xs expand-all-btn"
+ }],
+ }
+ }
+
+ add_item(node, view) {
+ frappe.prompt([
+ { label: __("Item"), fieldname: "item_code", fieldtype: "Link", options: "Item", reqd: 1 },
+ { label: __("Qty"), fieldname: "qty", default: 1.0, fieldtype: "Float", reqd: 1 },
+ ],
+ (data) => {
+ if (!node.data.parent_id) {
+ node.data.parent_id = this.frm.doc.name;
+ }
+
+ frappe.call({
+ method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_item",
+ args: {
+ parent: node.data.parent_id,
+ fg_item: node.data.value,
+ item_code: data.item_code,
+ fg_reference_id: node.data.name || this.frm.doc.name,
+ qty: data.qty,
+ },
+ callback: (r) => {
+ view.events.load_tree(r, node);
+ }
+ });
+ },
+ __("Add Item"),
+ __("Add"));
+ }
+
+ add_sub_assembly(node, view) {
+ let dialog = new frappe.ui.Dialog({
+ fields: view.events.get_sub_assembly_modal_fields(),
+ title: __("Add Sub Assembly"),
+ });
+
+ dialog.show();
+ view.events.set_default_qty(dialog);
+
+ dialog.set_primary_action(__("Add"), () => {
+ let bom_item = dialog.get_values();
+
+ if (!node.data?.parent_id) {
+ node.data.parent_id = this.frm.doc.name;
+ }
+
+ frappe.call({
+ method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_sub_assembly",
+ args: {
+ parent: node.data.parent_id,
+ fg_item: node.data.value,
+ fg_reference_id: node.data.name || this.frm.doc.name,
+ bom_item: bom_item,
+ },
+ callback: (r) => {
+ view.events.load_tree(r, node);
+ }
+ });
+
+ dialog.hide();
+ });
+
+ }
+
+ get_sub_assembly_modal_fields(read_only=false) {
+ return [
+ { label: __("Sub Assembly Item"), fieldname: "item_code", fieldtype: "Link", options: "Item", reqd: 1, read_only: read_only },
+ { fieldtype: "Column Break" },
+ { label: __("Qty"), fieldname: "qty", default: 1.0, fieldtype: "Float", reqd: 1, read_only: read_only },
+ { fieldtype: "Section Break" },
+ { label: __("Raw Materials"), fieldname: "items", fieldtype: "Table", reqd: 1,
+ fields: [
+ { label: __("Item"), fieldname: "item_code", fieldtype: "Link", options: "Item", reqd: 1, in_list_view: 1 },
+ { label: __("Qty"), fieldname: "qty", default: 1.0, fieldtype: "Float", reqd: 1, in_list_view: 1 },
+ ]
+ },
+ ]
+ }
+
+ convert_to_sub_assembly(node, view) {
+ let dialog = new frappe.ui.Dialog({
+ fields: view.events.get_sub_assembly_modal_fields(true),
+ title: __("Add Sub Assembly"),
+ });
+
+ dialog.set_values({
+ item_code: node.data.value,
+ qty: node.data.qty,
+ });
+
+ dialog.show();
+ view.events.set_default_qty(dialog);
+
+ dialog.set_primary_action(__("Add"), () => {
+ let bom_item = dialog.get_values();
+
+ frappe.call({
+ method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_sub_assembly",
+ args: {
+ parent: node.data.parent_id,
+ fg_item: node.data.value,
+ bom_item: bom_item,
+ fg_reference_id: node.data.name || this.frm.doc.name,
+ convert_to_sub_assembly: true,
+ },
+ callback: (r) => {
+ node.expandable = true;
+ view.events.load_tree(r, node);
+ }
+ });
+
+ dialog.hide();
+ });
+ }
+
+ set_default_qty(dialog) {
+ dialog.fields_dict.items.grid.fields_map.item_code.onchange = function (event) {
+ if (event) {
+ let name = $(event.currentTarget).closest('.grid-row').attr("data-name")
+ let item_row = dialog.fields_dict.items.grid.grid_rows_by_docname[name].doc;
+ item_row.qty = 1;
+ dialog.fields_dict.items.grid.refresh()
+ }
+ }
+ }
+
+ delete_node(node, view) {
+ frappe.confirm(__("Are you sure you want to delete this Item?"), () => {
+ frappe.call({
+ method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.delete_node",
+ args: {
+ parent: node.data.parent_id,
+ fg_item: node.data.value,
+ doctype: node.data.doctype,
+ docname: node.data.name,
+ },
+ callback: (r) => {
+ view.events.load_tree(r, node.parent_node);
+ }
+ });
+ });
+ }
+
+ edit_qty(node, view) {
+ let qty = node.data.qty || this.frm.doc.qty;
+ frappe.prompt([
+ { label: __("Qty"), fieldname: "qty", default: qty, fieldtype: "Float", reqd: 1 },
+ ],
+ (data) => {
+ let doctype = node.data.doctype || this.frm.doc.doctype;
+ let docname = node.data.name || this.frm.doc.name;
+
+ frappe.call({
+ method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.edit_qty",
+ args: {
+ doctype: doctype,
+ docname: docname,
+ qty: data.qty,
+ parent: node.data.parent_id,
+ },
+ callback: (r) => {
+ node.data.qty = data.qty;
+ let uom = node.data.uom || this.frm.doc.uom;
+ $(node.parent.get(0)).find(`[data-bom-qty-docname='${docname}']`).html(data.qty + " " + uom);
+ view.events.load_tree(r, node);
+ }
+ });
+ },
+ __("Edit Qty"),
+ __("Update"));
+ }
+
+ prepare_layout() {
+ let main_div = $(this.page)[0];
+
+ main_div.style.marginBottom = "15px";
+ $(main_div).find(".tree-children")[0].style.minHeight = "370px";
+ $(main_div).find(".tree-children")[0].style.maxHeight = "370px";
+ $(main_div).find(".tree-children")[0].style.overflowY = "auto";
+ }
+
+ load_tree(response, node) {
+ let item_row = "";
+ let parent_dom = ""
+ let total_amount = response.message.raw_material_cost;
+
+ frappe.views.trees["BOM Configurator"].tree.load_children(node);
+
+ while (true) {
+ item_row = response.message.items.filter(item => item.name === node.data.name);
+
+ if (item_row?.length) {
+ node.data.amount = item_row[0].amount;
+ total_amount = node.data.amount
+ } else {
+ total_amount = response.message.raw_material_cost;
+ }
+
+ parent_dom = $(node.parent.get(0));
+ total_amount = frappe.format(
+ total_amount, {
+ fieldtype: "Currency",
+ currency: this.frm.doc.currency
+ }
+ );
+
+ $($(parent_dom).find(".fg-item-amt")[0]).html(total_amount);
+
+ if (node.is_root) {
+ break;
+ }
+
+ node = node.parent_node;
+ }
+
+ }
+}
+
+frappe.ui.BOMConfigurator = BOMConfigurator;
\ No newline at end of file
diff --git a/erpnext/public/js/controllers/stock_controller.js b/erpnext/public/js/controllers/stock_controller.js
index 720423b..e9c409e 100644
--- a/erpnext/public/js/controllers/stock_controller.js
+++ b/erpnext/public/js/controllers/stock_controller.js
@@ -57,7 +57,8 @@
from_date: me.frm.doc.posting_date,
to_date: moment(me.frm.doc.modified).format('YYYY-MM-DD'),
company: me.frm.doc.company,
- show_cancelled_entries: me.frm.doc.docstatus === 2
+ show_cancelled_entries: me.frm.doc.docstatus === 2,
+ ignore_prepared_report: true
};
frappe.set_route("query-report", "Stock Ledger");
}, __("View"));
@@ -75,7 +76,8 @@
to_date: moment(me.frm.doc.modified).format('YYYY-MM-DD'),
company: me.frm.doc.company,
group_by: "Group by Voucher (Consolidated)",
- show_cancelled_entries: me.frm.doc.docstatus === 2
+ show_cancelled_entries: me.frm.doc.docstatus === 2,
+ ignore_prepared_report: true
};
frappe.set_route("query-report", "General Ledger");
}, __("View"));
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 59d2b15..ac5735b 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -277,7 +277,7 @@
}
setup_quality_inspection() {
- if(!in_list(["Delivery Note", "Sales Invoice", "Purchase Receipt", "Purchase Invoice"], this.frm.doc.doctype)) {
+ if(!in_list(["Delivery Note", "Sales Invoice", "Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"], this.frm.doc.doctype)) {
return;
}
@@ -289,7 +289,7 @@
this.frm.page.set_inner_btn_group_as_primary(__('Create'));
}
- const inspection_type = in_list(["Purchase Receipt", "Purchase Invoice"], this.frm.doc.doctype)
+ const inspection_type = in_list(["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"], this.frm.doc.doctype)
? "Incoming" : "Outgoing";
let quality_inspection_field = this.frm.get_docfield("items", "quality_inspection");
@@ -2067,6 +2067,7 @@
const me = this;
const dialog = new frappe.ui.Dialog({
title: __("Select Items for Quality Inspection"),
+ size: "extra-large",
fields: fields,
primary_action: function () {
const data = dialog.get_values();
diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js
index d7bea7b..966a9e1 100644
--- a/erpnext/public/js/erpnext.bundle.js
+++ b/erpnext/public/js/erpnext.bundle.js
@@ -28,5 +28,6 @@
import "./utils/landed_taxes_and_charges_common.js";
import "./utils/sales_common.js";
import "./controllers/buying.js";
+import "./utils/demo.js";
// import { sum } from 'frappe/public/utils/util.js'
diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js
index 934fd1f..ba200ef 100644
--- a/erpnext/public/js/setup_wizard.js
+++ b/erpnext/public/js/setup_wizard.js
@@ -40,6 +40,12 @@
{ fieldname: 'fy_start_date', label: __('Financial Year Begins On'), fieldtype: 'Date', reqd: 1 },
// end date should be hidden (auto calculated)
{ fieldname: 'fy_end_date', label: __('End Date'), fieldtype: 'Date', reqd: 1, hidden: 1 },
+ { fieldtype: "Section Break" },
+ {
+ fieldname: 'setup_demo',
+ label: __('Generate Demo Data for Exploration'),
+ fieldtype: 'Check',
+ description: 'If checked, we will create demo data for you to explore the system. This demo data can be erased later.'},
],
onload: function (slide) {
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index f456e5e..a3c10c6 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -114,6 +114,10 @@
},
view_serial_batch_nos: function(frm) {
+ if (!frm.doc?.items) {
+ return;
+ }
+
let bundle_ids = frm.doc.items.filter(d => d.serial_and_batch_bundle);
if (bundle_ids?.length) {
@@ -579,7 +583,9 @@
"conversion_factor": d.conversion_factor,
"qty": d.qty,
"rate": d.rate,
- "uom": d.uom
+ "uom": d.uom,
+ "fg_item": d.fg_item,
+ "fg_item_qty": d.fg_item_qty,
}
});
@@ -678,6 +684,37 @@
})
}
+ if (frm.doc.doctype == 'Purchase Order' && frm.doc.is_subcontracted && !frm.doc.is_old_subcontracting_flow) {
+ fields.push({
+ fieldtype:'Link',
+ fieldname:'fg_item',
+ options: 'Item',
+ reqd: 1,
+ in_list_view: 0,
+ read_only: 0,
+ disabled: 0,
+ label: __('Finished Good Item'),
+ get_query: () => {
+ return {
+ filters: {
+ 'is_stock_item': 1,
+ 'is_sub_contracted_item': 1,
+ 'default_bom': ['!=', '']
+ }
+ }
+ },
+ }, {
+ fieldtype:'Float',
+ fieldname:'fg_item_qty',
+ reqd: 1,
+ default: 0,
+ read_only: 0,
+ in_list_view: 0,
+ label: __('Finished Good Item Qty'),
+ precision: get_precision('fg_item_qty')
+ })
+ }
+
let dialog = new frappe.ui.Dialog({
title: __("Update Items"),
size: "extra-large",
diff --git a/erpnext/public/js/utils/demo.js b/erpnext/public/js/utils/demo.js
new file mode 100644
index 0000000..3ebc5ef
--- /dev/null
+++ b/erpnext/public/js/utils/demo.js
@@ -0,0 +1,91 @@
+$(document).on("toolbar_setup", function () {
+ if (frappe.boot.sysdefaults.demo_company) {
+ render_clear_demo_button();
+ }
+
+ // for first load after setup.
+ frappe.realtime.on("demo_data_complete", () => {
+ render_clear_demo_button();
+ });
+});
+
+function render_clear_demo_button() {
+ let wait_for_onboaring_tours = setInterval(() => {
+ if ($("#driver-page-overlay").length || $("#show-dialog").length) {
+ return;
+ }
+ setup_clear_demo_button();
+ clearInterval(wait_for_onboaring_tours);
+ }, 2000);
+}
+
+function setup_clear_demo_button() {
+ let message_string = __(
+ "Demo data is present on the system, erase data before starting real usage."
+ );
+ let $floatingBar = $(`
+ <div class="flex justify-content-center" style="width: 100%;">
+ <div class="flex justify-content-center flex-col shadow rounded p-2"
+ style="
+ background-color: #e0f2fe;
+ position: fixed;
+ bottom: 20px;
+ z-index: 1;">
+ <p style="margin: auto 0; padding-left: 10px; margin-right: 20px; font-size: 15px;">
+ ${message_string}
+ </p>
+ <button id="clear-demo" type="button"
+ class="
+ px-4
+ py-2
+ border
+ border-transparent
+ text-white
+ "
+ style="
+ margin: auto 0;
+ height: fit-content;
+ background-color: #007bff;
+ border-radius: 5px;
+ margin-right: 10px
+ "
+ >
+ Clear Demo Data
+ </button>
+
+ <a type="button" id="dismiss-demo-banner" class="text-muted" style="align-self: center">
+ <svg class="icon" style="">
+ <use class="" href="#icon-close"></use>
+ </svg>
+ </a>
+ </div>
+ </div>
+ `);
+
+ $("footer").append($floatingBar);
+
+ $("#clear-demo").on("click", function () {
+ frappe.confirm(
+ __("Are you sure you want to clear all demo data?"),
+ () => {
+ frappe.call({
+ method: "erpnext.setup.demo.clear_demo_data",
+ freeze: true,
+ freeze_message: __("Clearing Demo Data..."),
+ callback: function (r) {
+ frappe.ui.toolbar.clear_cache();
+ frappe.show_alert({
+ message: __("Demo data cleared"),
+ indicator: "green",
+ });
+ $("footer").remove($floatingBar);
+ },
+ });
+ }
+ );
+ });
+
+ $("#dismiss-demo-banner").on("click", function () {
+ $floatingBar.remove();
+ });
+}
diff --git a/erpnext/public/scss/point-of-sale.scss b/erpnext/public/scss/point-of-sale.scss
index c9d001c..ba64b59 100644
--- a/erpnext/public/scss/point-of-sale.scss
+++ b/erpnext/public/scss/point-of-sale.scss
@@ -53,7 +53,7 @@
.seperator {
margin-left: var(--margin-sm);
- margin-right: var(--margin-sm);
+ margin-right: var(--margin-md);
border-bottom: 1px solid var(--gray-300);
}
@@ -381,6 +381,7 @@
align-items: center;
padding: var(--padding-sm);
border-radius: var(--border-radius-md);
+ margin-right: var(--margin-sm);
&:hover {
background-color: var(--control-bg);
@@ -858,13 +859,10 @@
> .fields-section {
flex: 1;
- position: absolute;
display: flex;
flex-direction: column;
width: 50%;
height: 100%;
- top: 0;
- left: 0;
padding-bottom: var(--margin-md);
.invoice-fields {
@@ -1152,3 +1150,62 @@
}
}
}
+
+@media screen and (max-width: 620px) {
+ .point-of-sale-app {
+ grid-template-columns: repeat(1, minmax(0, 1fr));
+
+ > .items-selector {
+ grid-column: span 6 / span 1 !important;
+ > .items-container {
+ grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
+ }
+ }
+
+ > .item-details-container, .customer-cart-container {
+ grid-column: span 6 / span 1;
+ }
+
+ > .payment-container {
+ overflow: scroll;
+ > .fields-numpad-container {
+ flex-direction: column-reverse;
+ > .number-pad {
+ display: none;
+ }
+ > .fields-section {
+ width: 100%;
+ }
+ }
+ }
+
+ > .past-order-summary{
+ > .invoice-summary-wrapper {
+ width: 100%;
+ }
+ }
+
+ .numpad-totals {
+ > span {
+ padding: 0 5px;
+ font-size: var(--text-sm);
+ }
+ }
+
+ .col > * {
+ font-size: var(--text-sm) !important;
+ }
+
+ .control-input-wrapper {
+ padding-left: 0.15rem;
+ }
+
+ .pay-amount {
+ margin-left: 0.2rem;
+ }
+
+ .past-order-list {
+ grid-column: span 6 / span 1;
+ }
+ }
+}
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index 555db59..d351c3c 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -15,6 +15,7 @@
from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options
from frappe.model.utils.rename_doc import update_linked_doctypes
from frappe.utils import cint, cstr, flt, get_formatted_email, today
+from frappe.utils.nestedset import get_root_of
from frappe.utils.user import get_users_with_role
from erpnext.accounts.party import ( # noqa
@@ -80,6 +81,7 @@
validate_party_accounts(self)
self.validate_credit_limit_on_change()
self.set_loyalty_program()
+ self.set_territory_and_group()
self.check_customer_group_change()
self.validate_default_bank_account()
self.validate_internal_customer()
@@ -138,6 +140,12 @@
_("{0} is not a company bank account").format(frappe.bold(self.default_bank_account))
)
+ def set_territory_and_group(self):
+ if not self.territory:
+ self.territory = get_root_of("Territory")
+ if not self.customer_group:
+ self.customer_group = get_root_of("Customer Group")
+
def validate_internal_customer(self):
if not self.is_internal_customer:
self.represents_company = ""
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index ca669f6..cc141ff 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -108,18 +108,26 @@
and customer = %s",
(self.po_no, self.name, self.customer),
)
- if (
- so
- and so[0][0]
- and not cint(
+ if so and so[0][0]:
+ if cint(
frappe.db.get_single_value("Selling Settings", "allow_against_multiple_purchase_orders")
- )
- ):
- frappe.msgprint(
- _("Warning: Sales Order {0} already exists against Customer's Purchase Order {1}").format(
- so[0][0], self.po_no
+ ):
+ frappe.msgprint(
+ _("Warning: Sales Order {0} already exists against Customer's Purchase Order {1}").format(
+ frappe.bold(so[0][0]), frappe.bold(self.po_no)
+ )
)
- )
+ else:
+ frappe.throw(
+ _(
+ "Sales Order {0} already exists against Customer's Purchase Order {1}. To allow multiple Sales Orders, Enable {2} in {3}"
+ ).format(
+ frappe.bold(so[0][0]),
+ frappe.bold(self.po_no),
+ frappe.bold(_("'Allow Multiple Sales Orders Against a Customer's Purchase Order'")),
+ get_link_to_form("Selling Settings", "Selling Settings"),
+ )
+ )
def validate_for_items(self):
for d in self.get("items"):
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index c85a4fb..954393f 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -2055,7 +2055,7 @@
so.company = args.company or "_Test Company"
so.customer = args.customer or "_Test Customer"
so.currency = args.currency or "INR"
- so.po_no = args.po_no or "12345"
+ so.po_no = args.po_no or ""
if args.selling_price_list:
so.selling_price_list = args.selling_price_list
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json
index 045227f..6855012 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.json
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.json
@@ -20,6 +20,7 @@
"editable_price_list_rate",
"validate_selling_price",
"editable_bundle_item_rates",
+ "allow_negative_rates_for_items",
"sales_transactions_settings_section",
"so_required",
"dn_required",
@@ -85,7 +86,7 @@
"fieldname": "sales_update_frequency",
"fieldtype": "Select",
"label": "Sales Update Frequency in Company and Project",
- "options": "Each Transaction\nDaily\nMonthly",
+ "options": "Monthly\nEach Transaction\nDaily",
"reqd": 1
},
{
@@ -193,6 +194,12 @@
"fieldname": "dont_reserve_sales_order_qty_on_sales_return",
"fieldtype": "Check",
"label": "Don't Reserve Sales Order Qty on Sales Return"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_negative_rates_for_items",
+ "fieldtype": "Check",
+ "label": "Allow Negative rates for Items"
}
],
"icon": "fa fa-cog",
@@ -200,7 +207,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2023-02-04 12:37:53.380857",
+ "modified": "2023-08-14 20:33:05.693667",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling Settings",
@@ -229,4 +236,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
-}
+}
\ 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 720d142..db6255a 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -225,6 +225,7 @@
voucher.pos_opening_entry = this.pos_opening;
voucher.period_end_date = frappe.datetime.now_datetime();
voucher.posting_date = frappe.datetime.now_date();
+ voucher.posting_time = frappe.datetime.now_time();
frappe.set_route('Form', 'POS Closing Entry', voucher.name);
}
diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js
index 46490c4..193048f 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_cart.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js
@@ -286,7 +286,7 @@
this.item_is_selected = false;
this.$cart_container.find('.cart-item-wrapper').css("background-color", "");
} else {
- $cart_item.css("background-color", "var(--gray-50)");
+ $cart_item.css("background-color", "var(--control-bg)");
this.item_is_selected = true;
this.$cart_container.find('.cart-item-wrapper').not(item).css("background-color", "");
}
diff --git a/erpnext/setup/demo.py b/erpnext/setup/demo.py
new file mode 100644
index 0000000..926283f
--- /dev/null
+++ b/erpnext/setup/demo.py
@@ -0,0 +1,210 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import json
+import os
+from random import randint
+
+import frappe
+from frappe import _
+from frappe.utils import add_days, getdate
+
+from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
+from erpnext.accounts.utils import get_fiscal_year
+from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
+from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
+from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account
+
+
+def setup_demo_data():
+ from frappe.utils.telemetry import capture
+
+ capture("demo_data_creation_started", "erpnext")
+ try:
+ company = create_demo_company()
+ process_masters()
+ make_transactions(company)
+ frappe.cache.delete_keys("bootinfo")
+ frappe.publish_realtime("demo_data_complete")
+ except Exception:
+ frappe.log_error("Failed to create demo data")
+ capture("demo_data_creation_failed", "erpnext", properties={"exception": frappe.get_traceback()})
+ raise
+ capture("demo_data_creation_completed", "erpnext")
+
+
+@frappe.whitelist()
+def clear_demo_data():
+ from frappe.utils.telemetry import capture
+
+ frappe.only_for("System Manager")
+
+ capture("demo_data_erased", "erpnext")
+ try:
+ company = frappe.db.get_single_value("Global Defaults", "demo_company")
+ create_transaction_deletion_record(company)
+ clear_masters()
+ delete_company(company)
+ default_company = frappe.db.get_single_value("Global Defaults", "default_company")
+ frappe.db.set_default("company", default_company)
+ except Exception:
+ frappe.db.rollback()
+ frappe.log_error("Failed to erase demo data")
+ frappe.throw(
+ _("Failed to erase demo data, please delete the demo company manually."),
+ title=_("Could Not Delete Demo Data"),
+ )
+
+
+def create_demo_company():
+ company = frappe.db.get_all("Company")[0].name
+ company_doc = frappe.get_doc("Company", company)
+
+ # Make a dummy company
+ new_company = frappe.new_doc("Company")
+ new_company.company_name = company_doc.company_name + " (Demo)"
+ new_company.abbr = company_doc.abbr + "D"
+ new_company.enable_perpetual_inventory = 1
+ new_company.default_currency = company_doc.default_currency
+ new_company.country = company_doc.country
+ new_company.chart_of_accounts_based_on = "Standard Template"
+ new_company.chart_of_accounts = company_doc.chart_of_accounts
+ new_company.insert()
+
+ # Set Demo Company as default to
+ frappe.db.set_single_value("Global Defaults", "demo_company", new_company.name)
+ frappe.db.set_default("company", new_company.name)
+
+ bank_account = create_bank_account({"company_name": new_company.name})
+ frappe.db.set_value("Company", new_company.name, "default_bank_account", bank_account.name)
+
+ return new_company.name
+
+
+def process_masters():
+ for doctype in frappe.get_hooks("demo_master_doctypes"):
+ data = read_data_file_using_hooks(doctype)
+ if data:
+ for item in json.loads(data):
+ create_demo_record(item)
+
+
+def create_demo_record(doctype):
+ frappe.get_doc(doctype).insert(ignore_permissions=True)
+
+
+def make_transactions(company):
+ frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1)
+ start_date = get_fiscal_year(date=getdate())[1]
+
+ for doctype in frappe.get_hooks("demo_transaction_doctypes"):
+ data = read_data_file_using_hooks(doctype)
+ if data:
+ for item in json.loads(data):
+ create_transaction(item, company, start_date)
+
+ convert_order_to_invoices()
+ frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 0)
+
+
+def create_transaction(doctype, company, start_date):
+ document_type = doctype.get("doctype")
+ warehouse = get_warehouse(company)
+
+ if document_type == "Purchase Order":
+ posting_date = get_random_date(start_date, 1, 30)
+ else:
+ posting_date = get_random_date(start_date, 31, 364)
+
+ doctype.update(
+ {
+ "company": company,
+ "set_posting_time": 1,
+ "transaction_date": posting_date,
+ "schedule_date": posting_date,
+ "delivery_date": posting_date,
+ "set_warehouse": warehouse,
+ }
+ )
+
+ doc = frappe.get_doc(doctype)
+ doc.save(ignore_permissions=True)
+ doc.submit()
+
+
+def convert_order_to_invoices():
+ for document in ["Purchase Order", "Sales Order"]:
+ # Keep some orders intentionally unbilled/unpaid
+ for i, order in enumerate(
+ frappe.db.get_all(
+ document, filters={"docstatus": 1}, fields=["name", "transaction_date"], limit=6
+ )
+ ):
+
+ if document == "Purchase Order":
+ invoice = make_purchase_invoice(order.name)
+ elif document == "Sales Order":
+ invoice = make_sales_invoice(order.name)
+
+ invoice.set_posting_time = 1
+ invoice.posting_date = order.transaction_date
+ invoice.due_date = order.transaction_date
+ invoice.update_stock = 1
+ invoice.submit()
+
+ if i % 2 != 0:
+ payment = get_payment_entry(invoice.doctype, invoice.name)
+ payment.reference_no = invoice.name
+ payment.submit()
+
+
+def get_random_date(start_date, start_range, end_range):
+ return add_days(start_date, randint(start_range, end_range))
+
+
+def create_transaction_deletion_record(company):
+ transaction_deletion_record = frappe.new_doc("Transaction Deletion Record")
+ transaction_deletion_record.company = company
+ transaction_deletion_record.save(ignore_permissions=True)
+ transaction_deletion_record.submit()
+
+
+def clear_masters():
+ for doctype in frappe.get_hooks("demo_master_doctypes")[::-1]:
+ data = read_data_file_using_hooks(doctype)
+ if data:
+ for item in json.loads(data):
+ clear_demo_record(item)
+
+
+def clear_demo_record(document):
+ document_type = document.get("doctype")
+ del document["doctype"]
+
+ valid_columns = frappe.get_meta(document_type).get_valid_columns()
+
+ filters = document
+ for key in list(filters):
+ if key not in valid_columns:
+ filters.pop(key, None)
+
+ doc = frappe.get_doc(document_type, filters)
+ doc.delete(ignore_permissions=True)
+
+
+def delete_company(company):
+ frappe.db.set_single_value("Global Defaults", "demo_company", "")
+ frappe.delete_doc("Company", company, ignore_permissions=True)
+
+
+def read_data_file_using_hooks(doctype):
+ path = os.path.join(os.path.dirname(__file__), "demo_data")
+ with open(os.path.join(path, doctype + ".json"), "r") as f:
+ data = f.read()
+
+ return data
+
+
+def get_warehouse(company):
+ warehouses = frappe.db.get_all("Warehouse", {"company": company, "is_group": 0})
+ return warehouses[randint(0, 3)].name
diff --git a/erpnext/setup/demo_data/customer.json b/erpnext/setup/demo_data/customer.json
new file mode 100644
index 0000000..5e77e78
--- /dev/null
+++ b/erpnext/setup/demo_data/customer.json
@@ -0,0 +1,17 @@
+[
+ {
+ "doctype": "Customer",
+ "customer_group": "Demo Customer Group",
+ "customer_name": "Grant Plastics Ltd."
+ },
+ {
+ "doctype": "Customer",
+ "customer_group": "Demo Customer Group",
+ "customer_name": "West View Software Ltd."
+ },
+ {
+ "doctype": "Customer",
+ "customer_group": "Demo Customer Group",
+ "customer_name": "Palmer Productions Ltd."
+ }
+]
diff --git a/erpnext/setup/demo_data/customer_group.json b/erpnext/setup/demo_data/customer_group.json
new file mode 100644
index 0000000..7543335
--- /dev/null
+++ b/erpnext/setup/demo_data/customer_group.json
@@ -0,0 +1,6 @@
+[
+ {
+ "doctype": "Customer Group",
+ "customer_group_name": "Demo Customer Group"
+ }
+]
\ No newline at end of file
diff --git a/erpnext/setup/demo_data/item.json b/erpnext/setup/demo_data/item.json
new file mode 100644
index 0000000..330e114
--- /dev/null
+++ b/erpnext/setup/demo_data/item.json
@@ -0,0 +1,82 @@
+[
+ {
+ "doctype": "Item",
+ "item_group": "Demo Item Group",
+ "item_code": "SKU001",
+ "item_name": "T-shirt",
+ "gst_hsn_code": "999512",
+ "image": "https://images.pexels.com/photos/1484808/pexels-photo-1484808.jpeg"
+ },
+ {
+ "doctype": "Item",
+ "item_group": "Demo Item Group",
+ "item_code": "SKU002",
+ "item_name": "Laptop",
+ "gst_hsn_code": "999512",
+ "image": "https://images.pexels.com/photos/3999538/pexels-photo-3999538.jpeg"
+ },
+ {
+ "doctype": "Item",
+ "item_group": "Demo Item Group",
+ "item_code": "SKU003",
+ "item_name": "Book",
+ "gst_hsn_code": "999512",
+ "image": "https://images.pexels.com/photos/2422178/pexels-photo-2422178.jpeg"
+ },
+ {
+ "doctype": "Item",
+ "item_group": "Demo Item Group",
+ "item_code": "SKU004",
+ "item_name": "Smartphone",
+ "gst_hsn_code": "999512",
+ "image": "https://images.pexels.com/photos/1647976/pexels-photo-1647976.jpeg"
+ },
+ {
+ "doctype": "Item",
+ "item_group": "Demo Item Group",
+ "item_code": "SKU005",
+ "item_name": "Sneakers",
+ "gst_hsn_code": "999512",
+ "image": "https://images.pexels.com/photos/1598505/pexels-photo-1598505.jpeg"
+ },
+ {
+ "doctype": "Item",
+ "item_group": "Demo Item Group",
+ "item_code": "SKU006",
+ "item_name": "Coffee Mug",
+ "gst_hsn_code": "999512",
+ "image": "https://images.pexels.com/photos/585753/pexels-photo-585753.jpeg"
+ },
+ {
+ "doctype": "Item",
+ "item_group": "Demo Item Group",
+ "item_code": "SKU007",
+ "item_name": "Television",
+ "gst_hsn_code": "999512",
+ "image": "https://images.pexels.com/photos/8059376/pexels-photo-8059376.jpeg"
+ },
+ {
+ "doctype": "Item",
+ "item_group": "Demo Item Group",
+ "item_code": "SKU008",
+ "item_name": "Backpack",
+ "gst_hsn_code": "999512",
+ "image": "https://images.pexels.com/photos/3731256/pexels-photo-3731256.jpeg"
+ },
+ {
+ "doctype": "Item",
+ "item_group": "Demo Item Group",
+ "item_code": "SKU009",
+ "item_name": "Headphones",
+ "gst_hsn_code": "999512",
+ "image": "https://images.pexels.com/photos/3587478/pexels-photo-3587478.jpeg"
+ },
+ {
+ "doctype": "Item",
+ "item_group": "Demo Item Group",
+ "item_code": "SKU010",
+ "item_name": "Camera",
+ "gst_hsn_code": "999512",
+ "image": "https://images.pexels.com/photos/51383/photo-camera-subject-photographer-51383.jpeg"
+ }
+]
diff --git a/erpnext/setup/demo_data/item_group.json b/erpnext/setup/demo_data/item_group.json
new file mode 100644
index 0000000..f96944d
--- /dev/null
+++ b/erpnext/setup/demo_data/item_group.json
@@ -0,0 +1,6 @@
+[
+ {
+ "doctype": "Item Group",
+ "item_group_name": "Demo Item Group"
+ }
+]
diff --git a/erpnext/setup/demo_data/journal_entry.json b/erpnext/setup/demo_data/journal_entry.json
new file mode 100644
index 0000000..b751c7c
--- /dev/null
+++ b/erpnext/setup/demo_data/journal_entry.json
@@ -0,0 +1,25 @@
+[
+ {
+ "cheque_date": "2023-03-14",
+ "cheque_no": "33",
+ "doctype": "Journal Entry",
+ "accounts": [
+ {
+ "party_type": "Customer",
+ "party": "ABC Enterprises",
+ "credit_in_account_currency": 40000.0,
+ "debit_in_account_currency": 0.0,
+ "doctype": "Journal Entry Account",
+ "parentfield": "accounts",
+ },
+ {
+ "credit_in_account_currency": 0.0,
+ "debit_in_account_currency": 40000.0,
+ "doctype": "Journal Entry Account",
+ "parentfield": "accounts",
+ }
+ ],
+ "user_remark": "test",
+ "voucher_type": "Bank Entry"
+ }
+]
\ No newline at end of file
diff --git a/erpnext/setup/demo_data/payment_entry.json b/erpnext/setup/demo_data/payment_entry.json
new file mode 100644
index 0000000..c0767c3
--- /dev/null
+++ b/erpnext/setup/demo_data/payment_entry.json
@@ -0,0 +1,57 @@
+[
+ {
+ "doctype": "Payment Entry",
+ "payment_type": "Receive",
+ "party_type": "Customer",
+ "party": "ABC Enterprises",
+ "paid_amount": 67000,
+ "received_amount": 67000,
+ "reference_no": "#ref0001",
+ "source_exchange_rate": 1,
+ "target_exchange_rate": 1
+ },
+ {
+ "doctype": "Payment Entry",
+ "payment_type": "Receive",
+ "party_type": "Customer",
+ "party": "XYZ Corporation",
+ "paid_amount": 500000,
+ "received_amount": 500000,
+ "reference_no": "#ref0001",
+ "source_exchange_rate": 1,
+ "target_exchange_rate": 1
+ },
+ {
+ "doctype": "Payment Entry",
+ "payment_type": "Receive",
+ "party_type": "Customer",
+ "party": "KJPR Pvt. Ltd.",
+ "paid_amount": 300000,
+ "received_amount": 30000,
+ "reference_no": "#ref0001",
+ "source_exchange_rate": 1,
+ "target_exchange_rate": 1
+ },
+ {
+ "doctype": "Payment Entry",
+ "payment_type": "Pay",
+ "party_type": "Supplier",
+ "party": "DQ Industries",
+ "paid_amount": 85000,
+ "received_amount": 85000,
+ "reference_no": "#ref0005",
+ "source_exchange_rate": 1,
+ "target_exchange_rate": 1
+ },
+ {
+ "doctype": "Payment Entry",
+ "payment_type": "Pay",
+ "party_type": "Supplier",
+ "party": "KC Corp.",
+ "paid_amount": 100000,
+ "received_amount": 100000,
+ "reference_no": "#ref0006",
+ "source_exchange_rate": 1,
+ "target_exchange_rate": 1
+ }
+]
\ No newline at end of file
diff --git a/erpnext/setup/demo_data/purchase_order.json b/erpnext/setup/demo_data/purchase_order.json
new file mode 100644
index 0000000..318a865
--- /dev/null
+++ b/erpnext/setup/demo_data/purchase_order.json
@@ -0,0 +1,172 @@
+[
+ {
+ "conversion_rate": 1.0,
+ "supplier": "Zuckerman Security Ltd.",
+ "doctype": "Purchase Order",
+ "update_stock": 1,
+ "disable_rounded_total": 1,
+ "items": [
+ {
+ "doctype": "Purchase Order Item",
+ "item_code": "SKU001",
+ "parentfield": "items",
+ "qty": 100.0,
+ "rate": 400.0,
+ "conversion_factor": 1
+ }
+ ]
+ },
+ {
+ "conversion_rate": 1.0,
+ "supplier": "MA Inc.",
+ "doctype": "Purchase Order",
+ "update_stock": 1,
+ "disable_rounded_total": 1,
+ "items": [
+ {
+ "doctype": "Purchase Order Item",
+ "item_code": "SKU002",
+ "parentfield": "items",
+ "qty": 50.0,
+ "rate": 300.0,
+ "conversion_factor": 1
+ }
+ ]
+ },
+ {
+ "conversion_rate": 1.0,
+ "supplier": "Summit Traders Ltd.",
+ "doctype": "Purchase Order",
+ "update_stock": 1,
+ "disable_rounded_total": 1,
+ "items": [
+ {
+ "doctype": "Purchase Order Item",
+ "item_code": "SKU003",
+ "parentfield": "items",
+ "qty": 200.0,
+ "rate": 523.0,
+ "conversion_factor": 1
+ }
+ ]
+ },
+ {
+ "conversion_rate": 1.0,
+ "supplier": "Zuckerman Security Ltd.",
+ "doctype": "Purchase Order",
+ "update_stock": 1,
+ "disable_rounded_total": 1,
+ "items": [
+ {
+ "doctype": "Purchase Order Item",
+ "item_code": "SKU004",
+ "parentfield": "items",
+ "qty": 60.0,
+ "rate": 725.0,
+ "conversion_factor": 1
+ }
+ ]
+ },
+ {
+ "conversion_rate": 1.0,
+ "supplier": "MA Inc.",
+ "doctype": "Purchase Order",
+ "update_stock": 1,
+ "disable_rounded_total": 1,
+ "items": [
+ {
+ "doctype": "Purchase Order Item",
+ "item_code": "SKU005",
+ "parentfield": "items",
+ "qty": 182.0,
+ "rate": 222.0,
+ "conversion_factor": 1
+ }
+ ]
+ },
+ {
+ "conversion_rate": 1.0,
+ "supplier": "Summit Traders Ltd.",
+ "doctype": "Purchase Order",
+ "update_stock": 1,
+ "disable_rounded_total": 1,
+ "items": [
+ {
+ "doctype": "Purchase Order Item",
+ "item_code": "SKU006",
+ "parentfield": "items",
+ "qty": 250.0,
+ "rate": 420.0,
+ "conversion_factor": 1
+ }
+ ]
+ },
+ {
+ "conversion_rate": 1.0,
+ "supplier": "Zuckerman Security Ltd.",
+ "doctype": "Purchase Order",
+ "update_stock": 1,
+ "disable_rounded_total": 1,
+ "items": [
+ {
+ "doctype": "Purchase Order Item",
+ "item_code": "SKU007",
+ "parentfield": "items",
+ "qty": 190.0,
+ "rate": 375.0,
+ "conversion_factor": 1
+ }
+ ]
+ },
+ {
+ "conversion_rate": 1.0,
+ "supplier": "MA Inc.",
+ "doctype": "Purchase Order",
+ "update_stock": 1,
+ "disable_rounded_total": 1,
+ "items": [
+ {
+ "doctype": "Purchase Order Item",
+ "item_code": "SKU008",
+ "parentfield": "items",
+ "qty": 121.0,
+ "rate": 333.0,
+ "conversion_factor": 1
+ }
+ ]
+ },
+ {
+ "conversion_rate": 1.0,
+ "supplier": "Summit Traders Ltd.",
+ "doctype": "Purchase Order",
+ "update_stock": 1,
+ "disable_rounded_total": 1,
+ "items": [
+ {
+ "doctype": "Purchase Order Item",
+ "item_code": "SKU009",
+ "parentfield": "items",
+ "qty": 76.0,
+ "rate": 700.0,
+ "conversion_factor": 1
+ }
+ ]
+ },
+ {
+ "conversion_rate": 1.0,
+ "supplier": "Zuckerman Security Ltd.",
+ "doctype": "Purchase Order",
+ "update_stock": 1,
+ "disable_rounded_total": 1,
+ "items": [
+ {
+ "doctype": "Purchase Order Item",
+ "item_code": "SKU010",
+ "parentfield": "items",
+ "qty": 78.0,
+ "rate": 500.0,
+ "conversion_factor": 1
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/erpnext/setup/demo_data/sales_order.json b/erpnext/setup/demo_data/sales_order.json
new file mode 100644
index 0000000..29bffc3
--- /dev/null
+++ b/erpnext/setup/demo_data/sales_order.json
@@ -0,0 +1,127 @@
+[
+ {
+ "conversion_rate": 1.0,
+ "customer": "Grant Plastics Ltd.",
+ "doctype": "Sales Order",
+ "update_stock": 1,
+ "disable_rounded_total": 1,
+ "items": [
+ {
+ "doctype": "Sales Order Item",
+ "item_code": "SKU004",
+ "parentfield": "items",
+ "qty": 20.0,
+ "rate": 1000.0,
+ "conversion_factor": 1
+ }
+ ]
+ },
+ {
+ "conversion_rate": 1.0,
+ "customer": "West View Software Ltd.",
+ "doctype": "Sales Order",
+ "update_stock": 1,
+ "disable_rounded_total": 1,
+ "items": [
+ {
+ "doctype": "Sales Order Item",
+ "item_code": "SKU001",
+ "parentfield": "items",
+ "qty": 25.0,
+ "rate": 800.0,
+ "conversion_factor": 1
+ },
+ {
+ "doctype": "Sales Order Item",
+ "item_code": "SKU002",
+ "parentfield": "items",
+ "qty": 15.0,
+ "rate": 800.0,
+ "conversion_factor": 1
+ }
+ ]
+ },
+ {
+ "conversion_rate": 1.0,
+ "customer": "West View Software Ltd.",
+ "doctype": "Sales Order",
+ "update_stock": 1,
+ "disable_rounded_total": 1,
+ "items": [
+ {
+ "doctype": "Sales Order Item",
+ "item_code": "SKU003",
+ "parentfield": "items",
+ "qty": 100,
+ "rate": 500.0,
+ "conversion_factor": 1
+ },
+ {
+ "doctype": "Sales Order Item",
+ "item_code": "SKU006",
+ "parentfield": "items",
+ "qty": 100,
+ "rate": 890.0,
+ "conversion_factor": 1
+ },
+ {
+ "doctype": "Sales Order Item",
+ "item_code": "SKU007",
+ "parentfield": "items",
+ "qty": 100,
+ "rate": 900.0,
+ "conversion_factor": 1
+ }
+ ]
+ },
+ {
+ "conversion_rate": 1.0,
+ "customer": "Palmer Productions Ltd.",
+ "doctype": "Sales Order",
+ "update_stock": 1,
+ "disable_rounded_total": 1,
+ "items": [
+ {
+ "doctype": "Sales Order Item",
+ "item_code": "SKU005",
+ "parentfield": "items",
+ "qty": 150.0,
+ "rate": 100.0,
+ "conversion_factor": 1
+ }
+ ]
+ },
+ {
+ "conversion_rate": 1.0,
+ "customer": "Grant Plastics Ltd.",
+ "doctype": "Sales Order",
+ "update_stock": 1,
+ "disable_rounded_total": 1,
+ "items": [
+ {
+ "doctype": "Sales Order Item",
+ "item_code": "SKU008",
+ "parentfield": "items",
+ "qty": 20.0,
+ "rate": 500.0,
+ "conversion_factor": 1
+ },
+ {
+ "doctype": "Sales Order Item",
+ "item_code": "SKU009",
+ "parentfield": "items",
+ "qty": 40.0,
+ "rate": 300.0,
+ "conversion_factor": 1
+ },
+ {
+ "doctype": "Sales Order Item",
+ "item_code": "SKU010",
+ "parentfield": "items",
+ "qty": 50.0,
+ "rate": 900.0,
+ "conversion_factor": 1
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/erpnext/setup/demo_data/supplier.json b/erpnext/setup/demo_data/supplier.json
new file mode 100644
index 0000000..01a4e89
--- /dev/null
+++ b/erpnext/setup/demo_data/supplier.json
@@ -0,0 +1,17 @@
+[
+ {
+ "doctype": "Supplier",
+ "supplier_group": "Demo Supplier Group",
+ "supplier_name": "Zuckerman Security Ltd."
+ },
+ {
+ "doctype": "Supplier",
+ "supplier_group": "Demo Supplier Group",
+ "supplier_name": "MA Inc."
+ },
+ {
+ "doctype": "Supplier",
+ "supplier_group": "Demo Supplier Group",
+ "supplier_name": "Summit Traders Ltd."
+ }
+]
\ No newline at end of file
diff --git a/erpnext/setup/demo_data/supplier_group.json b/erpnext/setup/demo_data/supplier_group.json
new file mode 100644
index 0000000..17070bf
--- /dev/null
+++ b/erpnext/setup/demo_data/supplier_group.json
@@ -0,0 +1,6 @@
+[
+ {
+ "doctype": "Supplier Group",
+ "supplier_group_name": "Demo Supplier Group"
+ }
+]
\ No newline at end of file
diff --git a/erpnext/setup/doctype/company/test_company.py b/erpnext/setup/doctype/company/test_company.py
index fd2fe30..babd7dd 100644
--- a/erpnext/setup/doctype/company/test_company.py
+++ b/erpnext/setup/doctype/company/test_company.py
@@ -195,6 +195,22 @@
child_company.save()
self.test_basic_tree()
+ def test_demo_data(self):
+ from erpnext.setup.demo import clear_demo_data, setup_demo_data
+
+ setup_demo_data()
+ company_name = frappe.db.get_value("Company", {"name": ("like", "%(Demo)")})
+ self.assertTrue(company_name)
+
+ for transaction in frappe.get_hooks("demo_transaction_doctypes"):
+ self.assertTrue(frappe.db.exists(frappe.unscrub(transaction), {"company": company_name}))
+
+ clear_demo_data()
+ company_name = frappe.db.get_value("Company", {"name": ("like", "%(Demo)")})
+ self.assertFalse(company_name)
+ for transaction in frappe.get_hooks("demo_transaction_doctypes"):
+ self.assertFalse(frappe.db.exists(frappe.unscrub(transaction), {"company": company_name}))
+
def create_company_communication(doctype, docname):
comm = frappe.get_doc(
diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.json b/erpnext/setup/doctype/global_defaults/global_defaults.json
index 823d2ba..bd80e1d 100644
--- a/erpnext/setup/doctype/global_defaults/global_defaults.json
+++ b/erpnext/setup/doctype/global_defaults/global_defaults.json
@@ -12,7 +12,8 @@
"default_currency",
"hide_currency_symbol",
"disable_rounded_total",
- "disable_in_words"
+ "disable_in_words",
+ "demo_company"
],
"fields": [
{
@@ -71,6 +72,14 @@
"fieldtype": "Check",
"in_list_view": 1,
"label": "Disable In Words"
+ },
+ {
+ "fieldname": "demo_company",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "label": "Demo Company",
+ "options": "Company",
+ "read_only": 1
}
],
"icon": "fa fa-cog",
diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py
index 535c87d..ae6881b 100644
--- a/erpnext/setup/setup_wizard/operations/install_fixtures.py
+++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py
@@ -490,7 +490,7 @@
def create_bank_account(args):
if not args.get("bank_account"):
- return
+ args["bank_account"] = _("Bank Account")
company_name = args.get("company_name")
bank_account_group = frappe.db.get_value(
diff --git a/erpnext/setup/setup_wizard/setup_wizard.py b/erpnext/setup/setup_wizard/setup_wizard.py
index 65b268e..2da107e 100644
--- a/erpnext/setup/setup_wizard/setup_wizard.py
+++ b/erpnext/setup/setup_wizard/setup_wizard.py
@@ -5,7 +5,8 @@
import frappe
from frappe import _
-from .operations import install_fixtures as fixtures
+from erpnext.setup.demo import setup_demo_data
+from erpnext.setup.setup_wizard.operations import install_fixtures as fixtures
def get_setup_stages(args=None):
@@ -37,6 +38,11 @@
],
},
{
+ "status": _("Setting up demo data"),
+ "fail_msg": _("Failed to setup demo data"),
+ "tasks": [{"fn": setup_demo, "args": args, "fail_msg": _("Failed to setup demo data")}],
+ },
+ {
"status": _("Wrapping up"),
"fail_msg": _("Failed to login"),
"tasks": [{"fn": fin, "args": args, "fail_msg": _("Failed to login")}],
@@ -63,6 +69,11 @@
login_as_first_user(args)
+def setup_demo(args):
+ if args.get("setup_demo"):
+ frappe.enqueue(setup_demo_data, enqueue_after_commit=True, at_front=True)
+
+
def login_as_first_user(args):
if args.get("email") and hasattr(frappe.local, "login_manager"):
frappe.local.login_manager.login_as(args.get("email"))
diff --git a/erpnext/startup/boot.py b/erpnext/startup/boot.py
index db1cc49..bdbf8b4 100644
--- a/erpnext/startup/boot.py
+++ b/erpnext/startup/boot.py
@@ -61,6 +61,8 @@
)
bootinfo.party_account_types = frappe._dict(party_account_types)
+ bootinfo.sysdefaults.demo_company = frappe.db.get_single_value("Global Defaults", "demo_company")
+
def update_page_info(bootinfo):
bootinfo.page_info.update(
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 0ef3027..48b8ab7 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -703,7 +703,7 @@
def test_dn_billing_status_case1(self):
# SO -> DN -> SI
- so = make_sales_order()
+ so = make_sales_order(po_no="12345")
dn = create_dn_against_so(so.name, delivered_qty=2)
self.assertEqual(dn.status, "To Bill")
@@ -730,7 +730,7 @@
make_sales_invoice,
)
- so = make_sales_order()
+ so = make_sales_order(po_no="12345")
si = make_sales_invoice(so.name)
si.get("items")[0].qty = 5
@@ -774,7 +774,7 @@
frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1)
- so = make_sales_order()
+ so = make_sales_order(po_no="12345")
dn1 = make_delivery_note(so.name)
dn1.get("items")[0].qty = 2
@@ -820,7 +820,7 @@
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
- so = make_sales_order()
+ so = make_sales_order(po_no="12345")
si = make_sales_invoice(so.name)
si.submit()
@@ -1227,6 +1227,7 @@
self.assertEqual(get_reserved_qty(item, warehouse), 0 if dont_reserve_qty else qty_to_reserve)
def tearDown(self):
+ frappe.db.rollback()
frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0)
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index 1139c4b..9efae6a 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -656,7 +656,10 @@
"job_card_item": "job_card_item",
},
"postprocess": update_item,
- "condition": lambda doc: doc.ordered_qty < doc.stock_qty,
+ "condition": lambda doc: (
+ flt(doc.ordered_qty, doc.precision("ordered_qty"))
+ < flt(doc.stock_qty, doc.precision("ordered_qty"))
+ ),
},
},
target_doc,
diff --git a/erpnext/stock/doctype/material_request/material_request_dashboard.py b/erpnext/stock/doctype/material_request/material_request_dashboard.py
index 2bba52a..f91ea6a 100644
--- a/erpnext/stock/doctype/material_request/material_request_dashboard.py
+++ b/erpnext/stock/doctype/material_request/material_request_dashboard.py
@@ -6,6 +6,8 @@
"fieldname": "material_request",
"internal_links": {
"Sales Order": ["items", "sales_order"],
+ "Project": ["items", "project"],
+ "Cost Center": ["items", "cost_center"],
},
"transactions": [
{
@@ -15,5 +17,6 @@
{"label": _("Stock"), "items": ["Stock Entry", "Purchase Receipt", "Pick List"]},
{"label": _("Manufacturing"), "items": ["Work Order"]},
{"label": _("Internal Transfer"), "items": ["Sales Order"]},
+ {"label": _("Accounting Dimensions"), "items": ["Project", "Cost Center"]},
],
}
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json
index db9322f..914a9f3 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json
@@ -74,7 +74,7 @@
"fieldname": "reference_type",
"fieldtype": "Select",
"label": "Reference Type",
- "options": "\nPurchase Receipt\nPurchase Invoice\nDelivery Note\nSales Invoice\nStock Entry\nJob Card",
+ "options": "\nPurchase Receipt\nPurchase Invoice\nSubcontracting Receipt\nDelivery Note\nSales Invoice\nStock Entry\nJob Card",
"reqd": 1
},
{
@@ -245,7 +245,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-10-04 22:00:13.995221",
+ "modified": "2023-08-23 11:56:50.282878",
"modified_by": "Administrator",
"module": "Stock",
"name": "Quality Inspection",
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json
index 15bd2f0..d46b07a 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json
@@ -115,7 +115,8 @@
"fieldtype": "Dynamic Link",
"label": "Voucher No",
"no_copy": 1,
- "options": "voucher_type"
+ "options": "voucher_type",
+ "search_index": 1
},
{
"default": "0",
@@ -229,7 +230,8 @@
"fieldtype": "Data",
"label": "Voucher Detail No",
"no_copy": 1,
- "read_only": 1
+ "read_only": 1,
+ "search_index": 1
},
{
"allow_bulk_edit": 1,
@@ -248,7 +250,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2023-07-26 12:56:03.072224",
+ "modified": "2023-07-28 12:56:03.072224",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial and Batch Bundle",
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
index 43bd7ac..1f90c5b 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
@@ -3,7 +3,7 @@
import collections
import csv
-from collections import defaultdict
+from collections import Counter, defaultdict
from typing import Dict, List
import frappe
@@ -1197,6 +1197,7 @@
filters=[
["POS Invoice", "consolidated_invoice", "is", "not set"],
["POS Invoice", "docstatus", "=", 1],
+ ["POS Invoice", "is_return", "=", 0],
["POS Invoice Item", "item_code", "=", kwargs.item_code],
["POS Invoice", "name", "!=", kwargs.ignore_voucher_no],
],
@@ -1214,7 +1215,6 @@
for d in get_serial_batch_ledgers(kwargs.item_code, docstatus=1, name=ids):
ignore_serial_nos.append(d.serial_no)
- # Will be deprecated in v16
returned_serial_nos = []
for pos_invoice in pos_invoices:
if pos_invoice.serial_no:
@@ -1242,8 +1242,13 @@
child_doc, parent_doc, ignore_voucher_detail_no=kwargs.get("ignore_voucher_detail_no")
)
)
+ # Counter is used to create a hashmap of serial nos, which contains count of each serial no
+ # so we subtract returned serial nos from ignore serial nos after creating a counter of each to get the items which we need to ignore(which are sold)
- return list(set(ignore_serial_nos) - set(returned_serial_nos))
+ ignore_serial_nos_counter = Counter(ignore_serial_nos)
+ returned_serial_nos_counter = Counter(returned_serial_nos)
+
+ return list(ignore_serial_nos_counter - returned_serial_nos_counter)
def get_reserved_batches_for_pos(kwargs):
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 248b705..258a503 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -647,7 +647,7 @@
def update_distinct_item_warehouses(self, dependant_sle):
key = (dependant_sle.item_code, dependant_sle.warehouse)
- val = frappe._dict({"sle": dependant_sle, "dependent_voucher_detail_nos": []})
+ val = frappe._dict({"sle": dependant_sle})
if key not in self.distinct_item_warehouses:
self.distinct_item_warehouses[key] = val
@@ -661,6 +661,8 @@
if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
val.sle_changed = True
+ dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
+ val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
self.distinct_item_warehouses[key] = val
self.new_items_found = True
elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos):
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
index 94a2589..e374077 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
@@ -3,14 +3,91 @@
frappe.provide('erpnext.buying');
-erpnext.landed_cost_taxes_and_charges.setup_triggers("Subcontracting Receipt");
+erpnext.landed_cost_taxes_and_charges.setup_triggers('Subcontracting Receipt');
frappe.ui.form.on('Subcontracting Receipt', {
setup: (frm) => {
frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle'];
frm.get_field('supplied_items').grid.cannot_add_rows = true;
frm.get_field('supplied_items').grid.only_sortable();
+ frm.trigger('set_queries');
+ },
+ refresh: (frm) => {
+ if (frm.doc.docstatus > 0) {
+ frm.add_custom_button(__('Stock Ledger'), () => {
+ frappe.route_options = {
+ voucher_no: frm.doc.name,
+ from_date: frm.doc.posting_date,
+ to_date: moment(frm.doc.modified).format('YYYY-MM-DD'),
+ company: frm.doc.company,
+ show_cancelled_entries: frm.doc.docstatus === 2
+ };
+ frappe.set_route('query-report', 'Stock Ledger');
+ }, __('View'));
+
+ frm.add_custom_button(__('Accounting Ledger'), () => {
+ frappe.route_options = {
+ voucher_no: frm.doc.name,
+ from_date: frm.doc.posting_date,
+ to_date: moment(frm.doc.modified).format('YYYY-MM-DD'),
+ company: frm.doc.company,
+ group_by: 'Group by Voucher (Consolidated)',
+ show_cancelled_entries: frm.doc.docstatus === 2
+ };
+ frappe.set_route('query-report', 'General Ledger');
+ }, __('View'));
+ }
+
+ if (!frm.doc.is_return && frm.doc.docstatus === 1 && frm.doc.per_returned < 100) {
+ frm.add_custom_button(__('Subcontract Return'), () => {
+ frappe.model.open_mapped_doc({
+ method: 'erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return',
+ frm: frm
+ });
+ }, __('Create'));
+ frm.page.set_inner_btn_group_as_primary(__('Create'));
+ }
+
+ if (frm.doc.docstatus === 0) {
+ frm.add_custom_button(__('Subcontracting Order'), () => {
+ if (!frm.doc.supplier) {
+ frappe.throw({
+ title: __('Mandatory'),
+ message: __('Please Select a Supplier')
+ });
+ }
+
+ erpnext.utils.map_current_doc({
+ method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_subcontracting_receipt',
+ source_doctype: 'Subcontracting Order',
+ target: frm,
+ setters: {
+ supplier: frm.doc.supplier,
+ },
+ get_query_filters: {
+ docstatus: 1,
+ per_received: ['<', 100],
+ company: frm.doc.company
+ }
+ });
+ }, __('Get Items From'));
+
+ frm.fields_dict.supplied_items.grid.update_docfield_property('consumed_qty', 'read_only', frm.doc.__onload && frm.doc.__onload.backflush_based_on === 'BOM');
+ }
+
+ frm.trigger('setup_quality_inspection');
+ },
+
+ set_warehouse: (frm) => {
+ set_warehouse_in_children(frm.doc.items, 'warehouse', frm.doc.set_warehouse);
+ },
+
+ rejected_warehouse: (frm) => {
+ set_warehouse_in_children(frm.doc.items, 'rejected_warehouse', frm.doc.rejected_warehouse);
+ },
+
+ set_queries: (frm) => {
frm.set_query('set_warehouse', () => {
return {
filters: {
@@ -52,38 +129,36 @@
}
}));
- frm.set_query('expense_account', 'items', function () {
- return {
+ frm.set_query('expense_account', 'items', () => ({
query: 'erpnext.controllers.queries.get_expense_account',
filters: { 'company': frm.doc.company }
- };
- });
+ }));
- frm.set_query('batch_no', 'items', function(doc, cdt, cdn) {
+ frm.set_query('batch_no', 'items', (doc, cdt, cdn) => {
var row = locals[cdt][cdn];
return {
filters: {
item: row.item_code
}
- }
+ };
});
- frm.set_query('batch_no', 'supplied_items', function(doc, cdt, cdn) {
+ frm.set_query('batch_no', 'supplied_items', (doc, cdt, cdn) => {
var row = locals[cdt][cdn];
return {
filters: {
item: row.rm_item_code
}
- }
+ };
});
- frm.set_query("serial_and_batch_bundle", "supplied_items", (doc, cdt, cdn) => {
+ frm.set_query('serial_and_batch_bundle', 'supplied_items', (doc, cdt, cdn) => {
let row = locals[cdt][cdn];
return {
filters: {
'item_code': row.rm_item_code,
'voucher_type': doc.doctype,
- 'voucher_no': ["in", [doc.name, ""]],
+ 'voucher_no': ['in', [doc.name, '']],
'is_cancelled': 0,
}
}
@@ -101,7 +176,7 @@
let batch_no_field = frm.get_docfield('items', 'batch_no');
if (batch_no_field) {
- batch_no_field.get_route_options_for_new_doc = function(row) {
+ batch_no_field.get_route_options_for_new_doc = (row) => {
return {
'item': row.doc.item_code
}
@@ -109,85 +184,20 @@
}
},
- refresh: (frm) => {
- if (frm.doc.docstatus > 0) {
- frm.add_custom_button(__('Stock Ledger'), function () {
- frappe.route_options = {
- voucher_no: frm.doc.name,
- from_date: frm.doc.posting_date,
- to_date: moment(frm.doc.modified).format('YYYY-MM-DD'),
- company: frm.doc.company,
- show_cancelled_entries: frm.doc.docstatus === 2
- };
- frappe.set_route('query-report', 'Stock Ledger');
- }, __('View'));
-
- frm.add_custom_button(__('Accounting Ledger'), function () {
- frappe.route_options = {
- voucher_no: frm.doc.name,
- from_date: frm.doc.posting_date,
- to_date: moment(frm.doc.modified).format('YYYY-MM-DD'),
- company: frm.doc.company,
- group_by: 'Group by Voucher (Consolidated)',
- show_cancelled_entries: frm.doc.docstatus === 2
- };
- frappe.set_route('query-report', 'General Ledger');
- }, __('View'));
+ setup_quality_inspection: (frm) => {
+ if (!frm.is_new() && frm.doc.docstatus === 0 && !frm.doc.is_return) {
+ let transaction_controller = new erpnext.TransactionController({ frm: frm });
+ transaction_controller.setup_quality_inspection();
}
-
- if (!frm.doc.is_return && frm.doc.docstatus == 1 && frm.doc.per_returned < 100) {
- frm.add_custom_button(__('Subcontract Return'), function () {
- frappe.model.open_mapped_doc({
- method: 'erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return',
- frm: frm
- });
- }, __('Create'));
- frm.page.set_inner_btn_group_as_primary(__('Create'));
- }
-
- if (frm.doc.docstatus == 0) {
- frm.add_custom_button(__('Subcontracting Order'), function () {
- if (!frm.doc.supplier) {
- frappe.throw({
- title: __('Mandatory'),
- message: __('Please Select a Supplier')
- });
- }
-
- erpnext.utils.map_current_doc({
- method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_subcontracting_receipt',
- source_doctype: 'Subcontracting Order',
- target: frm,
- setters: {
- supplier: frm.doc.supplier,
- },
- get_query_filters: {
- docstatus: 1,
- per_received: ['<', 100],
- company: frm.doc.company
- }
- });
- }, __('Get Items From'));
-
- frm.fields_dict.supplied_items.grid.update_docfield_property('consumed_qty', 'read_only', frm.doc.__onload && frm.doc.__onload.backflush_based_on === 'BOM');
- }
- },
-
- set_warehouse: (frm) => {
- set_warehouse_in_children(frm.doc.items, 'warehouse', frm.doc.set_warehouse);
- },
-
- rejected_warehouse: (frm) => {
- set_warehouse_in_children(frm.doc.items, 'rejected_warehouse', frm.doc.rejected_warehouse);
},
});
frappe.ui.form.on('Landed Cost Taxes and Charges', {
- amount: function (frm, cdt, cdn) {
+ amount: (frm, cdt, cdn) => {
frm.events.set_base_amount(frm, cdt, cdn);
},
- expense_account: function (frm, cdt, cdn) {
+ expense_account: (frm, cdt, cdn) => {
frm.events.set_account_currency(frm, cdt, cdn);
}
});
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
index 60746d9..afe1b60 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
@@ -81,6 +81,9 @@
self.validate_posting_time()
self.validate_rejected_warehouse()
+ if not self.get("is_return"):
+ self.validate_inspection()
+
if getdate(self.posting_date) > getdate(nowdate()):
frappe.throw(_("Posting Date cannot be future date"))
@@ -270,17 +273,24 @@
status = "Draft"
elif self.docstatus == 1:
status = "Completed"
+
if self.is_return:
status = "Return"
- return_against = frappe.get_doc("Subcontracting Receipt", self.return_against)
- return_against.run_method("update_status")
elif self.per_returned == 100:
status = "Return Issued"
+
elif self.docstatus == 2:
status = "Cancelled"
+ if self.is_return:
+ frappe.get_doc("Subcontracting Receipt", self.return_against).update_status(
+ update_modified=update_modified
+ )
+
if status:
- frappe.db.set_value("Subcontracting Receipt", self.name, "status", status, update_modified)
+ frappe.db.set_value(
+ "Subcontracting Receipt", self.name, "status", status, update_modified=update_modified
+ )
def get_gl_entries(self, warehouse_account=None):
from erpnext.accounts.general_ledger import process_gl_map
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
index 887cba5..a170527 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
@@ -567,6 +567,64 @@
self.assertEqual(rm_item.rate, 100)
self.assertEqual(rm_item.amount, rm_item.consumed_qty * rm_item.rate)
+ def test_quality_inspection_for_subcontracting_receipt(self):
+ from erpnext.stock.doctype.quality_inspection.test_quality_inspection import (
+ create_quality_inspection,
+ )
+
+ set_backflush_based_on("BOM")
+ fg_item = "Subcontracted Item SA1"
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 1",
+ "qty": 5,
+ "rate": 100,
+ "fg_item": fg_item,
+ "fg_item_qty": 5,
+ },
+ ]
+ sco = get_subcontracting_order(service_items=service_items)
+ rm_items = get_rm_items(sco.supplied_items)
+ itemwise_details = make_stock_in_entry(rm_items=rm_items)
+ make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+ scr1 = make_subcontracting_receipt(sco.name)
+ scr1.save()
+
+ # Enable `Inspection Required before Purchase` in Item Master
+ frappe.db.set_value("Item", fg_item, "inspection_required_before_purchase", 1)
+
+ # ValidationError should be raised as Quality Inspection is not created/linked
+ self.assertRaises(frappe.ValidationError, scr1.submit)
+
+ qa = create_quality_inspection(
+ reference_type="Subcontracting Receipt",
+ reference_name=scr1.name,
+ inspection_type="Incoming",
+ item_code=fg_item,
+ )
+ scr1.reload()
+ self.assertEqual(scr1.items[0].quality_inspection, qa.name)
+
+ # SCR should be submitted successfully as Quality Inspection is set
+ scr1.submit()
+ qa.cancel()
+ scr1.reload()
+ scr1.cancel()
+
+ scr2 = make_subcontracting_receipt(sco.name)
+ scr2.save()
+
+ # Disable `Inspection Required before Purchase` in Item Master
+ frappe.db.set_value("Item", fg_item, "inspection_required_before_purchase", 0)
+
+ # ValidationError should not be raised as `Inspection Required before Purchase` is disabled
+ scr2.submit()
+
def make_return_subcontracting_receipt(**args):
args = frappe._dict(args)
diff --git a/erpnext/templates/includes/order/order_taxes.html b/erpnext/templates/includes/order/order_taxes.html
index 0060ab3..d7b9620 100644
--- a/erpnext/templates/includes/order/order_taxes.html
+++ b/erpnext/templates/includes/order/order_taxes.html
@@ -19,7 +19,7 @@
{{ d.description }}
</div>
<div class="item-grand-total col-4 text-right pr-0">
- {{ doc.get_formatted("net_total") }}
+ {{ d.get_formatted("base_tax_amount") }}
</div>
</div>
</div>