Merge branch 'develop' into SCR-SCRAP-ITEMS

This commit is contained in:
s-aga-r 2023-08-26 12:07:58 +05:30
commit 27d56461c5
41 changed files with 2660 additions and 288 deletions

View File

@ -437,12 +437,20 @@
},
"Sales": {
"Sales from Other Regions": {
"Sales from Other Region": {}
"Sales from Other Region": {
"account_type": "Income Account"
}
},
"Sales of same region": {
"Management Consultancy Fees 1": {},
"Sales Account": {},
"Sales of I/C": {}
"Management Consultancy Fees 1": {
"account_type": "Income Account"
},
"Sales Account": {
"account_type": "Income Account"
},
"Sales of I/C": {
"account_type": "Income Account"
}
}
},
"root_type": "Income"

View File

@ -69,8 +69,7 @@
"Persediaan Barang": {
"Persediaan Barang": {
"account_number": "1141.000",
"account_type": "Stock",
"is_group": 1
"account_type": "Stock"
},
"Uang Muka Pembelian": {
"Uang Muka Pembelian": {
@ -670,7 +669,8 @@
},
"Penjualan Barang Dagangan": {
"Penjualan": {
"account_number": "4110.000"
"account_number": "4110.000",
"account_type": "Income Account"
},
"Potongan Penjualan": {
"account_number": "4130.000"

View File

@ -271,9 +271,9 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
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
tax_amount = net_total * tax_details.rate / 100
else:
tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
tax_amount = net_total * tax_details.rate / 100
# once tds is deducted, not need to add vouchers in the invoice
voucher_wise_amount = {}

View File

@ -539,6 +539,10 @@ def get_round_off_account_and_cost_center(
"Company", company, ["round_off_account", "round_off_cost_center"]
) or [None, None]
# Use expense account as fallback
if not round_off_account:
round_off_account = frappe.get_cached_value("Company", company, "default_expense_account")
meta = frappe.get_meta(voucher_type)
# Give first preference to parent cost center for round off GLE

View File

@ -8,20 +8,17 @@ from erpnext import get_default_cost_center
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.accounts_receivable import execute
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
class TestAccountsReceivable(FrappeTestCase):
class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
def setUp(self):
frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'")
frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'")
frappe.db.sql("delete from `tabPayment Entry` where company='_Test Company 2'")
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'")
frappe.db.sql("delete from `tabPayment Ledger Entry` where company='_Test Company 2'")
frappe.db.sql("delete from `tabJournal Entry` where company='_Test Company 2'")
frappe.db.sql("delete from `tabExchange Rate Revaluation` where company='_Test Company 2'")
self.create_usd_account()
self.create_company()
self.create_customer()
self.create_item()
self.create_usd_receivable_account()
self.clear_old_entries()
def tearDown(self):
frappe.db.rollback()
@ -49,29 +46,84 @@ class TestAccountsReceivable(FrappeTestCase):
debtors_usd.account_type = debtors.account_type
self.debtors_usd = debtors_usd.save().name
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False):
frappe.set_user("Administrator")
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=100,
price_list_rate=100,
do_not_save=1,
)
if not no_payment_schedule:
si.append(
"payment_schedule",
dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
)
si.append(
"payment_schedule",
dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50),
)
si.append(
"payment_schedule",
dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20),
)
si = si.save()
if not do_not_submit:
si = si.submit()
return si
def create_payment_entry(self, docname):
pe = get_payment_entry("Sales Invoice", docname, bank_account=self.cash, party_amount=40)
pe.paid_from = self.debit_to
pe.insert()
pe.submit()
def create_credit_note(self, docname):
credit_note = create_sales_invoice(
company=self.company,
customer=self.customer,
item=self.item,
qty=-1,
debit_to=self.debit_to,
cost_center=self.cost_center,
is_return=1,
return_against=docname,
)
return credit_note
def test_accounts_receivable(self):
filters = {
"company": "_Test Company 2",
"company": self.company,
"based_on_payment_terms": 1,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"show_remarks": True,
}
# check invoice grand total and invoiced column's value for 3 payment terms
name = make_sales_invoice().name
si = self.create_sales_invoice()
name = si.name
report = execute(filters)
expected_data = [[100, 30], [100, 50], [100, 20]]
expected_data = [[100, 30, "No Remarks"], [100, 50, "No Remarks"], [100, 20, "No Remarks"]]
for i in range(3):
row = report[1][i - 1]
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced])
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks])
# check invoice grand total, invoiced, paid and outstanding column's value after payment
make_payment(name)
self.create_payment_entry(si.name)
report = execute(filters)
expected_data_after_payment = [[100, 50, 10, 40], [100, 20, 0, 20]]
@ -84,10 +136,10 @@ class TestAccountsReceivable(FrappeTestCase):
)
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
make_credit_note(name)
self.create_credit_note(si.name)
report = execute(filters)
expected_data_after_credit_note = [100, 0, 0, 40, -40, "Debtors - _TC2"]
expected_data_after_credit_note = [100, 0, 0, 40, -40, self.debit_to]
row = report[1][0]
self.assertEqual(
@ -108,21 +160,20 @@ class TestAccountsReceivable(FrappeTestCase):
"""
so = make_sales_order(
company="_Test Company 2",
customer="_Test Customer 2",
warehouse="Finished Goods - _TC2",
currency="EUR",
debit_to="Debtors - _TC2",
income_account="Sales - _TC2",
expense_account="Cost of Goods Sold - _TC2",
cost_center="Main - _TC2",
company=self.company,
customer=self.customer,
warehouse=self.warehouse,
debit_to=self.debit_to,
income_account=self.income_account,
expense_account=self.expense_account,
cost_center=self.cost_center,
)
pe = get_payment_entry(so.doctype, so.name)
pe = pe.save().submit()
filters = {
"company": "_Test Company 2",
"company": self.company,
"based_on_payment_terms": 0,
"report_date": today(),
"range1": 30,
@ -147,34 +198,32 @@ class TestAccountsReceivable(FrappeTestCase):
)
@change_settings(
"Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1}
"Accounts Settings",
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
)
def test_exchange_revaluation_for_party(self):
"""
Exchange Revaluation for party on Receivable/Payable shoule be included
Exchange Revaluation for party on Receivable/Payable should be included
"""
company = "_Test Company 2"
customer = "_Test Customer 2"
# Using Exchange Gain/Loss account for unrealized as well.
company_doc = frappe.get_doc("Company", company)
company_doc = frappe.get_doc("Company", self.company)
company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account
company_doc.save()
si = make_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si.currency = "USD"
si.conversion_rate = 0.90
si.conversion_rate = 80
si.debit_to = self.debtors_usd
si = si.save().submit()
# Exchange Revaluation
err = frappe.new_doc("Exchange Rate Revaluation")
err.company = company
err.company = self.company
err.posting_date = today()
accounts = err.get_accounts_data()
err.extend("accounts", accounts)
err.accounts[0].new_exchange_rate = 0.95
err.accounts[0].new_exchange_rate = 85
row = err.accounts[0]
row.new_balance_in_base_currency = flt(
row.new_exchange_rate * flt(row.balance_in_account_currency)
@ -189,7 +238,7 @@ class TestAccountsReceivable(FrappeTestCase):
je = je.submit()
filters = {
"company": company,
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
@ -198,7 +247,7 @@ class TestAccountsReceivable(FrappeTestCase):
}
report = execute(filters)
expected_data_for_err = [0, -5, 0, 5]
expected_data_for_err = [0, -500, 0, 500]
row = [x for x in report[1] if x.voucher_type == je.doctype and x.voucher_no == je.name][0]
self.assertEqual(
expected_data_for_err,
@ -214,46 +263,43 @@ class TestAccountsReceivable(FrappeTestCase):
"""
Payment against credit/debit note should be considered against the parent invoice
"""
company = "_Test Company 2"
customer = "_Test Customer 2"
si1 = make_sales_invoice()
si1 = self.create_sales_invoice()
pe = get_payment_entry("Sales Invoice", si1.name, bank_account="Cash - _TC2")
pe.paid_from = "Debtors - _TC2"
pe = get_payment_entry(si1.doctype, si1.name, bank_account=self.cash)
pe.paid_from = self.debit_to
pe.insert()
pe.submit()
cr_note = make_credit_note(si1.name)
cr_note = self.create_credit_note(si1.name)
si2 = make_sales_invoice()
si2 = self.create_sales_invoice()
# manually link cr_note with si2 using journal entry
je = frappe.new_doc("Journal Entry")
je.company = company
je.company = self.company
je.voucher_type = "Credit Note"
je.posting_date = today()
debit_account = "Debtors - _TC2"
debit_entry = {
"account": debit_account,
"account": self.debit_to,
"party_type": "Customer",
"party": customer,
"party": self.customer,
"debit": 100,
"debit_in_account_currency": 100,
"reference_type": cr_note.doctype,
"reference_name": cr_note.name,
"cost_center": "Main - _TC2",
"cost_center": self.cost_center,
}
credit_entry = {
"account": debit_account,
"account": self.debit_to,
"party_type": "Customer",
"party": customer,
"party": self.customer,
"credit": 100,
"credit_in_account_currency": 100,
"reference_type": si2.doctype,
"reference_name": si2.name,
"cost_center": "Main - _TC2",
"cost_center": self.cost_center,
}
je.append("accounts", debit_entry)
@ -261,7 +307,7 @@ class TestAccountsReceivable(FrappeTestCase):
je = je.save().submit()
filters = {
"company": company,
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
@ -271,64 +317,254 @@ class TestAccountsReceivable(FrappeTestCase):
report = execute(filters)
self.assertEqual(report[1], [])
def test_group_by_party(self):
si1 = self.create_sales_invoice(do_not_submit=True)
si1.posting_date = add_days(today(), -1)
si1.save().submit()
si2 = self.create_sales_invoice(do_not_submit=True)
si2.items[0].rate = 85
si2.save().submit()
def make_sales_invoice(no_payment_schedule=False, do_not_submit=False):
frappe.set_user("Administrator")
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"group_by_party": True,
}
report = execute(filters)[1]
self.assertEqual(len(report), 5)
si = create_sales_invoice(
company="_Test Company 2",
customer="_Test Customer 2",
currency="EUR",
warehouse="Finished Goods - _TC2",
debit_to="Debtors - _TC2",
income_account="Sales - _TC2",
expense_account="Cost of Goods Sold - _TC2",
cost_center="Main - _TC2",
do_not_save=1,
)
# assert voucher rows
expected_voucher_rows = [
[100.0, 100.0, 100.0, 100.0],
[85.0, 85.0, 85.0, 85.0],
]
voucher_rows = []
for x in report[0:2]:
voucher_rows.append(
[x.invoiced, x.outstanding, x.invoiced_in_account_currency, x.outstanding_in_account_currency]
)
self.assertEqual(expected_voucher_rows, voucher_rows)
if not no_payment_schedule:
si.append(
"payment_schedule",
dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
# assert total rows
expected_total_rows = [
[self.customer, 185.0, 185.0], # party total
{}, # empty row for padding
["Total", 185.0, 185.0], # grand total
]
party_total_row = report[2]
self.assertEqual(
expected_total_rows[0],
[
party_total_row.get("party"),
party_total_row.get("invoiced"),
party_total_row.get("outstanding"),
],
)
si.append(
"payment_schedule",
dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50),
)
si.append(
"payment_schedule",
dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20),
empty_row = report[3]
self.assertEqual(expected_total_rows[1], empty_row)
grand_total_row = report[4]
self.assertEqual(
expected_total_rows[2],
[
grand_total_row.get("party"),
grand_total_row.get("invoiced"),
grand_total_row.get("outstanding"),
],
)
si = si.save()
def test_future_payments(self):
si = self.create_sales_invoice()
pe = get_payment_entry(si.doctype, si.name)
pe.posting_date = add_days(today(), 1)
pe.paid_amount = 90.0
pe.references[0].allocated_amount = 90.0
pe.save().submit()
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"show_future_payments": True,
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
if not do_not_submit:
si = si.submit()
expected_data = [100.0, 100.0, 10.0, 90.0]
return si
row = report[0]
self.assertEqual(
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
)
pe.cancel()
# full payment in future date
pe = get_payment_entry(si.doctype, si.name)
pe.posting_date = add_days(today(), 1)
pe.save().submit()
report = execute(filters)[1]
self.assertEqual(len(report), 1)
expected_data = [100.0, 100.0, 0.0, 100.0]
row = report[0]
self.assertEqual(
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
)
def make_payment(docname):
pe = get_payment_entry("Sales Invoice", docname, bank_account="Cash - _TC2", party_amount=40)
pe.paid_from = "Debtors - _TC2"
pe.insert()
pe.submit()
pe.cancel()
# over payment in future date
pe = get_payment_entry(si.doctype, si.name)
pe.posting_date = add_days(today(), 1)
pe.paid_amount = 110
pe.save().submit()
report = execute(filters)[1]
self.assertEqual(len(report), 2)
expected_data = [[100.0, 0.0, 100.0, 0.0, 100.0], [0.0, 10.0, -10.0, -10.0, 0.0]]
for idx, row in enumerate(report):
self.assertEqual(
expected_data[idx],
[row.invoiced, row.paid, row.outstanding, row.remaining_balance, row.future_amount],
)
def test_sales_person(self):
sales_person = (
frappe.get_doc({"doctype": "Sales Person", "sales_person_name": "John Clark", "enabled": True})
.insert()
.submit()
)
si = self.create_sales_invoice(do_not_submit=True)
si.append("sales_team", {"sales_person": sales_person.name, "allocated_percentage": 100})
si.save().submit()
def make_credit_note(docname):
credit_note = create_sales_invoice(
company="_Test Company 2",
customer="_Test Customer 2",
currency="EUR",
qty=-1,
warehouse="Finished Goods - _TC2",
debit_to="Debtors - _TC2",
income_account="Sales - _TC2",
expense_account="Cost of Goods Sold - _TC2",
cost_center="Main - _TC2",
is_return=1,
return_against=docname,
)
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"sales_person": sales_person.name,
"show_sales_person": True,
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
return credit_note
expected_data = [100.0, 100.0, sales_person.name]
row = report[0]
self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.sales_person])
def test_cost_center_filter(self):
si = self.create_sales_invoice()
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"cost_center": self.cost_center,
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
expected_data = [100.0, 100.0, self.cost_center]
row = report[0]
self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.cost_center])
def test_customer_group_filter(self):
si = self.create_sales_invoice()
cus_group = frappe.db.get_value("Customer", self.customer, "customer_group")
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"customer_group": cus_group,
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
expected_data = [100.0, 100.0, cus_group]
row = report[0]
self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.customer_group])
filters.update({"customer_group": "Individual"})
report = execute(filters)[1]
self.assertEqual(len(report), 0)
def test_party_account_filter(self):
si1 = self.create_sales_invoice()
self.customer2 = (
frappe.get_doc(
{
"doctype": "Customer",
"customer_name": "Jane Doe",
"type": "Individual",
"default_currency": "USD",
}
)
.insert()
.submit()
)
si2 = self.create_sales_invoice(do_not_submit=True)
si2.posting_date = add_days(today(), -1)
si2.customer = self.customer2
si2.currency = "USD"
si2.conversion_rate = 80
si2.debit_to = self.debtors_usd
si2.save().submit()
# Filter on company currency receivable account
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"party_account": self.debit_to,
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
expected_data = [100.0, 100.0, self.debit_to, si1.currency]
row = report[0]
self.assertEqual(
expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency]
)
# Filter on USD receivable account
filters.update({"party_account": self.debtors_usd})
report = execute(filters)[1]
self.assertEqual(len(report), 1)
expected_data = [8000.0, 8000.0, self.debtors_usd, si2.currency]
row = report[0]
self.assertEqual(
expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency]
)
# without filter on party account
filters.pop("party_account")
report = execute(filters)[1]
self.assertEqual(len(report), 2)
expected_data = [
[8000.0, 8000.0, 100.0, 100.0, self.debtors_usd, si2.currency],
[100.0, 100.0, 100.0, 100.0, self.debit_to, si1.currency],
]
for idx, row in enumerate(report):
self.assertEqual(
expected_data[idx],
[
row.invoiced,
row.outstanding,
row.invoiced_in_account_currency,
row.outstanding_in_account_currency,
row.party_account,
row.account_currency,
],
)

View File

@ -58,6 +58,9 @@ def get_data(filters):
def get_asset_categories(filters):
condition = ""
if filters.get("asset_category"):
condition += " and asset_category = %(asset_category)s"
return frappe.db.sql(
"""
SELECT asset_category,
@ -98,15 +101,25 @@ def get_asset_categories(filters):
0
end), 0) as cost_of_scrapped_asset
from `tabAsset`
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {}
group by asset_category
""",
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
""".format(
condition
),
{
"to_date": filters.to_date,
"from_date": filters.from_date,
"company": filters.company,
"asset_category": filters.get("asset_category"),
},
as_dict=1,
)
def get_assets(filters):
condition = ""
if filters.get("asset_category"):
condition = " and a.asset_category = '{}'".format(filters.get("asset_category"))
return frappe.db.sql(
"""
SELECT results.asset_category,
@ -138,7 +151,7 @@ def get_assets(filters):
aca.parent = a.asset_category and aca.company_name = %(company)s
join `tabCompany` company on
company.name = %(company)s
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0}
group by a.asset_category
union
SELECT a.asset_category,
@ -154,10 +167,12 @@ def get_assets(filters):
end), 0) as depreciation_eliminated_during_the_period,
0 as depreciation_amount_during_the_period
from `tabAsset` a
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0}
group by a.asset_category) as results
group by results.asset_category
""",
""".format(
condition
),
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
as_dict=1,
)

View File

@ -257,7 +257,7 @@ def get_tds_docs(filters):
}
party = frappe.get_all(filters.get("party_type"), pluck="name")
query_filters.update({"against": ("in", party)})
or_filters.update({"against": ("in", party), "voucher_type": "Journal Entry"})
if filters.get("party"):
del query_filters["account"]
@ -294,7 +294,7 @@ def get_tds_docs(filters):
if journal_entries:
journal_entry_party_map = get_journal_entry_party_map(journal_entries)
get_doc_info(journal_entries, "Journal Entry", tax_category_map)
get_doc_info(journal_entries, "Journal Entry", tax_category_map, net_total_map)
return (
tds_documents,
@ -309,7 +309,11 @@ def get_journal_entry_party_map(journal_entries):
journal_entry_party_map = {}
for d in frappe.db.get_all(
"Journal Entry Account",
{"parent": ("in", journal_entries), "party_type": "Supplier", "party": ("is", "set")},
{
"parent": ("in", journal_entries),
"party_type": ("in", ("Supplier", "Customer")),
"party": ("is", "set"),
},
["parent", "party"],
):
if d.parent not in journal_entry_party_map:
@ -320,41 +324,29 @@ def get_journal_entry_party_map(journal_entries):
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",
"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"]
common_fields = ["name", "tax_withholding_category"]
fields_dict = {
"Purchase Invoice": ["base_tax_withholding_net_total", "grand_total", "base_total"],
"Sales Invoice": ["base_net_total", "grand_total", "base_total"],
"Payment Entry": ["paid_amount", "paid_amount_after_tax", "base_paid_amount"],
"Journal Entry": ["total_amount"],
}
entries = frappe.get_all(doctype, filters={"name": ("in", vouchers)}, fields=fields)
entries = frappe.get_all(
doctype, filters={"name": ("in", vouchers)}, fields=common_fields + fields_dict[doctype]
)
for entry in entries:
tax_category_map.update({entry.name: entry.tax_withholding_category})
if doctype == "Purchase Invoice":
net_total_map.update(
{entry.name: [entry.base_tax_withholding_net_total, entry.grand_total, entry.base_total]}
)
value = [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]})
value = [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]}
)
value = [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount]
else:
value = [entry.total_amount] * 3
net_total_map.update({entry.name: value})
def get_tax_rate_map(filters):

View File

@ -60,7 +60,6 @@ class AccountsTestMixin:
self.income_account = "Sales - " + abbr
self.expense_account = "Cost of Goods Sold - " + abbr
self.debit_to = "Debtors - " + abbr
self.debit_usd = "Debtors USD - " + abbr
self.cash = "Cash - " + abbr
self.creditors = "Creditors - " + abbr
self.retained_earnings = "Retained Earnings - " + abbr
@ -105,6 +104,28 @@ class AccountsTestMixin:
new_acc.save()
setattr(self, acc.attribute_name, new_acc.name)
def create_usd_receivable_account(self):
account_name = "Debtors USD"
if not frappe.db.get_value(
"Account", filters={"account_name": account_name, "company": self.company}
):
acc = frappe.new_doc("Account")
acc.account_name = account_name
acc.parent_account = "Accounts Receivable - " + self.company_abbr
acc.company = self.company
acc.account_currency = "USD"
acc.account_type = "Receivable"
acc.insert()
else:
name = frappe.db.get_value(
"Account",
filters={"account_name": account_name, "company": self.company},
fieldname="name",
pluck=True,
)
acc = frappe.get_doc("Account", name)
self.debtors_usd = acc.name
def clear_old_entries(self):
doctype_list = [
"GL Entry",
@ -113,6 +134,8 @@ class AccountsTestMixin:
"Purchase Invoice",
"Payment Entry",
"Journal Entry",
"Sales Order",
"Exchange Rate Revaluation",
]
for doctype in doctype_list:
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()

View File

@ -908,9 +908,9 @@ def get_outstanding_invoices(
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
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")

View File

@ -345,6 +345,8 @@ def make_return_doc(
elif doctype == "Purchase Invoice":
# look for Print Heading "Debit Note"
doc.select_print_heading = frappe.get_cached_value("Print Heading", _("Debit Note"))
if source.tax_withholding_category:
doc.set_onload("supplier_tds", source.tax_withholding_category)
for tax in doc.get("taxes") or []:
if tax.charge_type == "Actual":

View File

@ -599,6 +599,7 @@ class StockController(AccountsController):
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",
}

View File

@ -198,7 +198,7 @@ website_route_rules = [
]
standard_portal_menu_items = [
{"title": "Projects", "route": "/project", "reference_doctype": "Project"},
{"title": "Projects", "route": "/project", "reference_doctype": "Project", "role": "Customer"},
{
"title": "Request for Quotations",
"route": "/rfq",
@ -290,6 +290,7 @@ has_website_permission = {
"Delivery Note": "erpnext.controllers.website_list_for_contact.has_website_permission",
"Issue": "erpnext.support.doctype.issue.issue.has_website_permission",
"Timesheet": "erpnext.controllers.website_list_for_contact.has_website_permission",
"Project": "erpnext.controllers.website_list_for_contact.has_website_permission",
}
before_tests = "erpnext.setup.utils.before_tests"

View File

@ -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",

View File

@ -206,6 +206,7 @@ class BOM(WebsiteGenerator):
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 @@ class BOM(WebsiteGenerator):
# 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 @@ class BOM(WebsiteGenerator):
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 @@ def get_valuation_rate(data):
.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")

View File

@ -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}));

View File

@ -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
}

View File

@ -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

View File

@ -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"];
}
},
};

View File

@ -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

View File

@ -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": []
}

View File

@ -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

View File

@ -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 &amp; 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 &amp; 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",
@ -336,6 +336,13 @@
"type": "URL",
"url": "https://frappe.school/courses/manufacturing?utm_source=in_app"
},
{
"color": "Grey",
"doc_view": "List",
"label": "BOM Creator",
"link_to": "BOM Creator",
"type": "DocType"
},
{
"color": "Grey",
"doc_view": "List",

View File

@ -263,6 +263,7 @@ erpnext.patches.v15_0.saudi_depreciation_warning
erpnext.patches.v15_0.delete_saudi_doctypes
erpnext.patches.v14_0.show_loan_management_deprecation_warning
execute:frappe.rename_doc("Report", "TDS Payable Monthly", "Tax Withholding Details", force=True)
erpnext.patches.v14_0.delete_education_module_portal_menu_items
[post_model_sync]
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')

View File

@ -43,9 +43,18 @@ def execute():
frappe.delete_doc("Number Card", card, ignore_missing=True, force=True)
doctypes = frappe.get_all("DocType", {"module": "education", "custom": 0}, pluck="name")
for doctype in doctypes:
frappe.delete_doc("DocType", doctype, ignore_missing=True)
portal_settings = frappe.get_doc("Portal Settings")
for row in portal_settings.get("menu"):
if row.reference_doctype in doctypes:
row.delete()
portal_settings.save()
frappe.delete_doc("Module Def", "Education", ignore_missing=True, force=True)
click.secho(

View File

@ -0,0 +1,13 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import frappe
def execute():
doctypes = frappe.get_all("DocType", {"module": "education", "custom": 0}, pluck="name")
items = frappe.get_all(
"Portal Menu Item", filters={"reference_doctype": ("in", doctypes)}, pluck="name"
)
for item in items:
frappe.delete_doc("Portal Menu Item", item, ignore_missing=True, force=True)

View File

@ -10,9 +10,11 @@ from frappe.model.document import Document
from frappe.query_builder import Interval
from frappe.query_builder.functions import Count, CurDate, Date, UnixTimestamp
from frappe.utils import add_days, flt, get_datetime, get_time, get_url, nowtime, today
from frappe.utils.user import is_website_user
from erpnext import get_default_company
from erpnext.controllers.queries import get_filters_cond
from erpnext.controllers.website_list_for_contact import get_customers_suppliers
from erpnext.setup.doctype.holiday_list.holiday_list import is_holiday
@ -318,9 +320,20 @@ def get_timeline_data(doctype: str, name: str) -> dict[int, int]:
def get_project_list(
doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified"
):
user = frappe.session.user
customers, suppliers = get_customers_suppliers("Project", frappe.session.user)
ignore_permissions = False
if is_website_user():
if not filters:
filters = []
if customers:
filters.append([doctype, "customer", "in", customers])
ignore_permissions = True
meta = frappe.get_meta(doctype)
if not filters:
filters = []
fields = "distinct *"
@ -351,18 +364,26 @@ def get_project_list(
limit_start=limit_start,
limit_page_length=limit_page_length,
order_by=order_by,
ignore_permissions=ignore_permissions,
)
def get_list_context(context=None):
return {
"show_sidebar": True,
"show_search": True,
"no_breadcrumbs": True,
"title": _("Projects"),
"get_list": get_project_list,
"row_template": "templates/includes/projects/project_row.html",
}
from erpnext.controllers.website_list_for_contact import get_list_context
list_context = get_list_context(context)
list_context.update(
{
"show_sidebar": True,
"show_search": True,
"no_breadcrumbs": True,
"title": _("Projects"),
"get_list": get_project_list,
"row_template": "templates/includes/projects/project_row.html",
}
)
return list_context
@frappe.whitelist()

View File

@ -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;

View File

@ -277,7 +277,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
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 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
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 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
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();

View File

@ -45,7 +45,8 @@ erpnext.setup.slides_settings = [
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.'},
description: __('If checked, we will create demo data for you to explore the system. This demo data can be erased later.')
},
],
onload: function (slide) {

View File

@ -114,6 +114,10 @@ $.extend(erpnext.utils, {
},
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) {

View File

@ -15,6 +15,7 @@ from frappe.model.mapper import get_mapped_doc
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 @@ class Customer(TransactionBase):
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 @@ class Customer(TransactionBase):
_("{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 = ""

View File

@ -48,8 +48,8 @@ erpnext.PointOfSale.PastOrderSummary = class {
const email_dialog = new frappe.ui.Dialog({
title: 'Email Receipt',
fields: [
{fieldname: 'email_id', fieldtype: 'Data', options: 'Email', label: 'Email ID'},
// {fieldname:'remarks', fieldtype:'Text', label:'Remarks (if any)'}
{fieldname: 'email_id', fieldtype: 'Data', options: 'Email', label: 'Email ID', reqd: 1},
{fieldname:'content', fieldtype:'Small Text', label:'Message (if any)'}
],
primary_action: () => {
this.send_email();
@ -243,6 +243,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
send_email() {
const frm = this.events.get_frm();
const recipients = this.email_dialog.get_values().email_id;
const content = this.email_dialog.get_values().content;
const doc = this.doc || frm.doc;
const print_format = frm.pos_print_format;
@ -251,6 +252,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
args: {
recipients: recipients,
subject: __(frm.meta.name) + ': ' + doc.name,
content: content ? content : __(frm.meta.name) + ': ' + doc.name,
doctype: doc.doctype,
name: doc.name,
send_email: 1,

View File

@ -2,19 +2,16 @@
{
"doctype": "Customer",
"customer_group": "Demo Customer Group",
"territory": "All Territories",
"customer_name": "Grant Plastics Ltd."
},
{
"doctype": "Customer",
"customer_group": "Demo Customer Group",
"territory": "All Territories",
"customer_name": "West View Software Ltd."
},
{
"doctype": "Customer",
"customer_group": "Demo Customer Group",
"territory": "All Territories",
"customer_name": "Palmer Productions Ltd."
}
]
]

View File

@ -403,14 +403,20 @@ class Company(NestedSet):
self._set_default_account(default_account, default_accounts.get(default_account))
if not self.default_income_account:
income_account = frappe.db.get_value(
"Account", {"account_name": _("Sales"), "company": self.name, "is_group": 0}
income_account = frappe.db.get_all(
"Account",
filters={"company": self.name, "is_group": 0},
or_filters={
"account_name": ("in", [_("Sales"), _("Sales Account")]),
"account_type": "Income Account",
},
pluck="name",
)
if not income_account:
income_account = frappe.db.get_value(
"Account", {"account_name": _("Sales Account"), "company": self.name}
)
if income_account:
income_account = income_account[0]
else:
income_account = None
self.db_set("default_income_account", income_account)

View File

@ -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",

View File

@ -10,14 +10,91 @@ frappe.ui.form.on('Subcontracting Receipt', {
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: {
company: frm.doc.company,
is_group: 0
}
};
}
});
frm.set_query('rejected_warehouse', () => {
@ -26,7 +103,7 @@ frappe.ui.form.on('Subcontracting Receipt', {
company: frm.doc.company,
is_group: 0
}
};
}
});
frm.set_query('supplier_warehouse', () => {
@ -35,7 +112,7 @@ frappe.ui.form.on('Subcontracting Receipt', {
company: frm.doc.company,
is_group: 0
}
};
}
});
frm.set_query('warehouse', 'items', () => ({
@ -56,7 +133,7 @@ frappe.ui.form.on('Subcontracting Receipt', {
return {
query: 'erpnext.controllers.queries.get_expense_account',
filters: { 'company': frm.doc.company }
};
}
});
frm.set_query('batch_no', 'items', (doc, cdt, cdn) => {
@ -96,7 +173,7 @@ frappe.ui.form.on('Subcontracting Receipt', {
'item_code': row.doc.rm_item_code,
'voucher_type': frm.doc.doctype,
}
};
}
}
let batch_no_field = frm.get_docfield('items', 'batch_no');
@ -105,80 +182,15 @@ frappe.ui.form.on('Subcontracting Receipt', {
return {
'item': row.doc.item_code
}
};
}
}
},
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'));
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'), () => {
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');
}
},
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);
},
get_scrap_items: (frm) => {
@ -235,7 +247,7 @@ frappe.ui.form.on('Subcontracting Receipt Item', {
items_remove: (frm) => {
set_missing_values(frm);
}
},
});
frappe.ui.form.on('Subcontracting Receipt Supplied Item', {
@ -247,7 +259,7 @@ frappe.ui.form.on('Subcontracting Receipt Supplied Item', {
let set_warehouse_in_children = (child_table, warehouse_field, warehouse) => {
let transaction_controller = new erpnext.TransactionController();
transaction_controller.autofill_warehouse(child_table, warehouse_field, warehouse);
};
}
let set_missing_values = (frm) => {
frappe.call({
@ -257,4 +269,4 @@ let set_missing_values = (frm) => {
if (!r.exc) frm.refresh();
},
});
};
}

View File

@ -47,6 +47,9 @@ class SubcontractingReceipt(SubcontractingController):
self.reset_supplied_items()
self.validate_posting_time()
if not self.get("is_return"):
self.validate_inspection()
if getdate(self.posting_date) > getdate(nowdate()):
frappe.throw(_("Posting Date cannot be future date"))

View File

@ -567,6 +567,64 @@ class TestSubcontractingReceipt(FrappeTestCase):
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)

View File

@ -3279,7 +3279,7 @@ Quality Feedback,Commentaires sur la qualité,
Quality Feedback Template,Modèle de commentaires sur la qualité,
Rules for applying different promotional schemes.,Règles d'application de différents programmes promotionnels.,
Show {0},Montrer {0},
"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Caractères spéciaux sauf &quot;-&quot;, &quot;#&quot;, &quot;.&quot;, &quot;/&quot;, &quot;{{&quot; Et &quot;}}&quot; non autorisés dans les séries de nommage {0}",
"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Caractères spéciaux sauf &quot;-&quot;, &quot;#&quot;, &quot;.&quot;, &quot;/&quot;, &quot;{{&quot; Et &quot;}}&quot; non autorisés dans les masques de numérotation {0}",
Target Details,Détails de la cible,
{0} already has a Parent Procedure {1}.,{0} a déjà une procédure parent {1}.,
API,API,
@ -3292,7 +3292,7 @@ Group By,Par groupe,
Invalid URL,URL invalide,
Landscape,Paysage,
Last Sync On,Dernière synchronisation le,
Naming Series,Nom de série,
Naming Series,Masque de numérotation,
No data to export,Aucune donnée à exporter,
Portrait,Portrait,
Print Heading,Imprimer Titre,
@ -3962,7 +3962,7 @@ Please set {0},Veuillez définir {0},supplier
Draft,Brouillon,"docstatus,=,0"
Cancelled,Annulé,"docstatus,=,2"
Please setup Instructor Naming System in Education > Education Settings,Veuillez configurer le système de dénomination de l'instructeur dans Éducation&gt; Paramètres de l'éducation,
Please set Naming Series for {0} via Setup > Settings > Naming Series,Veuillez définir la série de noms pour {0} via Configuration&gt; Paramètres&gt; Série de noms,
Please set Naming Series for {0} via Setup > Settings > Naming Series,Veuillez définir le masque de numérotation pour {0} via Configuration&gt; Paramètres&gt; Série de noms,
UOM Conversion factor ({0} -> {1}) not found for item: {2},Facteur de conversion UdM ({0} -&gt; {1}) introuvable pour l'article: {2},
Item Code > Item Group > Brand,Code article&gt; Groupe d'articles&gt; Marque,
Customer > Customer Group > Territory,Client&gt; Groupe de clients&gt; Territoire,
@ -3973,7 +3973,7 @@ Fetch Serial Numbers based on FIFO,Récupérer les numéros de série basés sur
"Outward taxable supplies(other than zero rated, nil rated and exempted)","Fournitures taxables sortantes (autres que détaxées, nulles et exonérées)",
"To allow different rates, disable the {0} checkbox in {1}.","Pour autoriser différents tarifs, désactivez la {0} case à cocher dans {1}.",
Asset{} {assets_link} created for {},Élément {} {assets_link} créé pour {},
Row {}: Asset Naming Series is mandatory for the auto creation for item {},Ligne {}: la série de noms d'éléments est obligatoire pour la création automatique de l'élément {},
Row {}: Asset Naming Series is mandatory for the auto creation for item {},Ligne {}: Le masque de numérotation d'éléments est obligatoire pour la création automatique de l'élément {},
Assets not created for {0}. You will have to create asset manually.,Éléments non créés pour {0}. Vous devrez créer un actif manuellement.,
{0} {1} has accounting entries in currency {2} for company {3}. Please select a receivable or payable account with currency {2}.,{0} {1} a des écritures comptables dans la devise {2} pour l'entreprise {3}. Veuillez sélectionner un compte à recevoir ou à payer avec la devise {2}.,
Invalid Account,Compte invalide,
@ -3997,7 +3997,7 @@ Advanced Settings,Réglages avancés,
Path,Chemin,
Components,Composants,
Verified By,Vérifié Par,
Invalid naming series (. missing) for {0},Série de noms non valide (. Manquante) pour {0},
Invalid naming series (. missing) for {0},Masque de numérotation non valide (. Manquante) pour {0},
Filter Based On,Filtre basé sur,
Reqd by date,Reqd par date,
Manufacturer Part Number <b>{0}</b> is invalid,Le numéro de <b>pièce du</b> fabricant <b>{0}</b> n'est pas valide,
@ -5587,7 +5587,7 @@ Student Admission Program,Programme d'admission des étudiants,
Minimum Age,Âge Minimum,
Maximum Age,Âge Maximum,
Application Fee,Frais de Dossier,
Naming Series (for Student Applicant),Nom de série (pour un candidat étudiant),
Naming Series (for Student Applicant),Masque de numérotation (pour un candidat étudiant),
LMS Only,LMS seulement,
EDU-APP-.YYYY.-,EDU-APP-YYYY.-,
Application Date,Date de la Candidature,
@ -6074,7 +6074,7 @@ Hotel Reservation User,Utilisateur chargé des réservations d'hôtel,
Hotel Room Reservation Item,Article de réservation de la chambre d'hôtel,
Hotel Settings,Paramètres d'Hotel,
Default Taxes and Charges,Taxes et frais par défaut,
Default Invoice Naming Series,Numéro de série par défaut pour les factures,
Default Invoice Naming Series,Masque de numérotation par défaut pour les factures,
HR,RH,
Date on which this component is applied,Date à laquelle ce composant est appliqué,
Salary Slip,Fiche de Paie,
@ -7136,7 +7136,7 @@ Default Unit of Measure,Unité de Mesure par Défaut,
Maintain Stock,Maintenir Stock,
Standard Selling Rate,Prix de Vente Standard,
Auto Create Assets on Purchase,Création automatique d'actifs à l'achat,
Asset Naming Series,Nom de série de l'actif,
Asset Naming Series,Masque de numérotation de l'actif,
Over Delivery/Receipt Allowance (%),Surlivrance / indemnité de réception (%),
Barcodes,Codes-barres,
Shelf Life In Days,Durée de conservation en jours,
@ -7155,7 +7155,7 @@ Serial Nos and Batches,N° de Série et Lots,
Has Batch No,A un Numéro de Lot,
Automatically Create New Batch,Créer un Nouveau Lot Automatiquement,
Batch Number Series,Série de numéros de lots,
"Example: ABCD.#####. If series is set and Batch No is not mentioned in transactions, then automatic batch number will be created based on this series. If you always want to explicitly mention Batch No for this item, leave this blank. Note: this setting will take priority over the Naming Series Prefix in Stock Settings.","Exemple: ABCD. #####. Si la série est définie et que le numéro de lot n'est pas mentionné dans les transactions, un numéro de lot sera automatiquement créé en avec cette série. Si vous préferez mentionner explicitement et systématiquement le numéro de lot pour cet article, laissez ce champ vide. Remarque: ce paramètre aura la priorité sur le préfixe de la série dans les paramètres de stock.",
"Example: ABCD.#####. If series is set and Batch No is not mentioned in transactions, then automatic batch number will be created based on this series. If you always want to explicitly mention Batch No for this item, leave this blank. Note: this setting will take priority over the Naming Series Prefix in Stock Settings.","Exemple: ABCD. #####. Si le masque est définie et que le numéro de lot n'est pas mentionné dans les transactions, un numéro de lot sera automatiquement créé en avec ce masque. Si vous préferez mentionner explicitement et systématiquement le numéro de lot pour cet article, laissez ce champ vide. Remarque: ce paramètre aura la priorité sur le préfixe du masque dans les paramètres de stock.",
Has Expiry Date,A une date d'expiration,
Retain Sample,Conserver l'échantillon,
Max Sample Quantity,Quantité maximum d'échantillon,
@ -7455,8 +7455,8 @@ Inter Warehouse Transfer Settings,Paramètres de transfert entre entrepôts,
Freeze Stock Entries,Geler les Entrées de Stocks,
Stock Frozen Upto,Stock Gelé Jusqu'au,
Batch Identification,Identification par lots,
Use Naming Series,Utiliser la série de noms,
Naming Series Prefix,Préfix du nom de série,
Use Naming Series,Utiliser le masque de numérotation,
Naming Series Prefix,Préfix du masque de numérotation,
UOM Category,Catégorie d'unité de mesure (UdM),
UOM Conversion Detail,Détails de Conversion de l'UdM,
Variant Field,Champ de Variante,
@ -7914,7 +7914,7 @@ Is Inter State,Est Inter State,
Purchase Details,Détails d'achat,
Depreciation Posting Date,Date comptable de l'amortissement,
"By default, the Supplier Name is set as per the Supplier Name entered. If you want Suppliers to be named by a ","Par défaut, le nom du fournisseur est défini selon le nom du fournisseur saisi. Si vous souhaitez que les fournisseurs soient nommés par un",
choose the 'Naming Series' option.,choisissez l'option 'Naming Series'.,
choose the 'Naming Series' option.,choisissez l'option 'Masque de numérotation'.,
Configure the default Price List when creating a new Purchase transaction. Item prices will be fetched from this Price List.,Configurez la liste de prix par défaut lors de la création d'une nouvelle transaction d'achat. Les prix des articles seront extraits de cette liste de prix.,
"If this option is configured 'Yes', ERPNext will prevent you from creating a Purchase Invoice or Receipt without creating a Purchase Order first. This configuration can be overridden for a particular supplier by enabling the 'Allow Purchase Invoice Creation Without Purchase Order' checkbox in the Supplier master.","Si cette option est configurée «Oui», ERPNext vous empêchera de créer une facture d'achat ou un reçu sans créer d'abord une Commande d'Achat. Cette configuration peut être remplacée pour un fournisseur particulier en cochant la case «Autoriser la création de facture d'achat sans commmande d'achat» dans la fiche fournisseur.",
"If this option is configured 'Yes', ERPNext will prevent you from creating a Purchase Invoice without creating a Purchase Receipt first. This configuration can be overridden for a particular supplier by enabling the 'Allow Purchase Invoice Creation Without Purchase Receipt' checkbox in the Supplier master.","Si cette option est configurée «Oui», ERPNext vous empêchera de créer une facture d'achat sans créer d'abord un reçu d'achat. Cette configuration peut être remplacée pour un fournisseur particulier en cochant la case &quot;Autoriser la création de facture d'achat sans reçu d'achat&quot; dans la fiche fournisseur.",
@ -8871,7 +8871,7 @@ Auto Insert Item Price If Missing,Création du prix de l'article dans les listes
Update Existing Price List Rate,Mise a jour automatique du prix dans les listes de prix,
Show Barcode Field in Stock Transactions,Afficher le champ Code Barre dans les transactions de stock,
Convert Item Description to Clean HTML in Transactions,Convertir les descriptions d'articles en HTML valide lors des transactions,
Have Default Naming Series for Batch ID?,Nom de série par défaut pour les Lots ou Séries,
Have Default Naming Series for Batch ID?,Masque de numérotation par défaut pour les Lots ou Séries,
"The percentage you are allowed to transfer more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed transfer 110 units","Le pourcentage de quantité que vous pourrez réceptionner en plus de la quantité commandée. Par exemple, vous avez commandé 100 unités, votre pourcentage de dépassement est de 10%, vous pourrez réceptionner 110 unités"
Allowed Items,Articles autorisés,
Party Specific Item,Restriction d'article disponible,
@ -8925,3 +8925,15 @@ Enable Reviews and Ratings,Activer les avis et notes,
Enable Recommendations,Activer les recommendations,
Item Search Settings,Paramétrage de la recherche d'article,
Purchase demande,Demande de materiel,
Internal Customer,Client interne
Internal Supplier,Fournisseur interne
Contact & Address,Contact et Adresse
Primary Address and Contact,Adresse et contact principal
Supplier Primary Contact,Contact fournisseur principal
Supplier Primary Address,Adresse fournisseur principal
From Opportunity,Depuis l'opportunité
Default Receivable Accounts,Compte de débit par défaut
Receivable Accounts,Compte de débit
Mention if a non-standard receivable account,Veuillez mentionner s'il s'agit d'un compte débiteur non standard
Allow Purchase,Autoriser à l'achat
Inventory Settings,Paramétrage de l'inventaire

Can't render this file because it is too large.