Merge branch 'develop' into fix-consolidation-precision-error

This commit is contained in:
Deepesh Garg 2022-09-04 13:18:10 +05:30 committed by GitHub
commit 1a61d4e8a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 575 additions and 377 deletions

View File

@ -14,8 +14,8 @@ pos* @nextchamp-saqib
erpnext/buying/ @rohitwaghchaure @s-aga-r erpnext/buying/ @rohitwaghchaure @s-aga-r
erpnext/maintenance/ @rohitwaghchaure @s-aga-r erpnext/maintenance/ @rohitwaghchaure @s-aga-r
erpnext/manufacturing/ @rohitwaghchaure @s-aga-r erpnext/manufacturing/ @rohitwaghchaure @s-aga-r
erpnext/quality_management/ @marination @rohitwaghchaure @s-aga-r erpnext/quality_management/ @rohitwaghchaure @s-aga-r
erpnext/stock/ @marination @rohitwaghchaure @s-aga-r erpnext/stock/ @rohitwaghchaure @s-aga-r
erpnext/crm/ @NagariaHussain erpnext/crm/ @NagariaHussain
erpnext/education/ @rutwikhdev erpnext/education/ @rutwikhdev

View File

@ -22,7 +22,8 @@
"amount", "amount",
"account_currency", "account_currency",
"amount_in_account_currency", "amount_in_account_currency",
"delinked" "delinked",
"remarks"
], ],
"fields": [ "fields": [
{ {
@ -136,12 +137,17 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Finance Book", "label": "Finance Book",
"options": "Finance Book" "options": "Finance Book"
},
{
"fieldname": "remarks",
"fieldtype": "Text",
"label": "Remarks"
} }
], ],
"in_create": 1, "in_create": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2022-07-11 09:13:54.379168", "modified": "2022-08-22 15:32:56.629430",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Ledger Entry", "name": "Payment Ledger Entry",

View File

@ -34,4 +34,4 @@ class ProcessDeferredAccounting(Document):
filters={"against_voucher_type": self.doctype, "against_voucher": self.name}, filters={"against_voucher_type": self.doctype, "against_voucher": self.name},
) )
make_gl_entries(gl_entries=gl_entries, cancel=1) make_gl_entries(gl_map=gl_entries, cancel=1)

View File

@ -57,3 +57,16 @@ class TestProcessDeferredAccounting(unittest.TestCase):
] ]
check_gl_entries(self, si.name, expected_gle, "2019-01-10") check_gl_entries(self, si.name, expected_gle, "2019-01-10")
def test_pda_submission_and_cancellation(self):
pda = frappe.get_doc(
dict(
doctype="Process Deferred Accounting",
posting_date="2019-01-01",
start_date="2019-01-01",
end_date="2019-01-31",
type="Income",
)
)
pda.submit()
pda.cancel()

View File

@ -282,7 +282,6 @@
"label": "Discount (%) on Price List Rate with Margin", "label": "Discount (%) on Price List Rate with Margin",
"oldfieldname": "adj_rate", "oldfieldname": "adj_rate",
"oldfieldtype": "Float", "oldfieldtype": "Float",
"precision": "2",
"print_hide": 1 "print_hide": 1
}, },
{ {
@ -846,7 +845,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-06-17 05:33:15.335912", "modified": "2022-08-26 12:06:31.205417",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Item", "name": "Sales Invoice Item",

View File

@ -178,6 +178,11 @@ frappe.query_reports["Accounts Receivable"] = {
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1 "hidden": 1
}, },
{
"fieldname": "show_remarks",
"label": __("Show Remarks"),
"fieldtype": "Check",
},
{ {
"fieldname": "customer_name", "fieldname": "customer_name",
"label": __("Customer Name"), "label": __("Customer Name"),

View File

@ -119,6 +119,7 @@ class ReceivablePayableReport(object):
party_account=ple.account, party_account=ple.account,
posting_date=ple.posting_date, posting_date=ple.posting_date,
account_currency=ple.account_currency, account_currency=ple.account_currency,
remarks=ple.remarks,
invoiced=0.0, invoiced=0.0,
paid=0.0, paid=0.0,
credit_note=0.0, credit_note=0.0,
@ -697,6 +698,7 @@ class ReceivablePayableReport(object):
ple.account_currency, ple.account_currency,
ple.amount, ple.amount,
ple.amount_in_account_currency, ple.amount_in_account_currency,
ple.remarks,
) )
.where(ple.delinked == 0) .where(ple.delinked == 0)
.where(Criterion.all(self.qb_selection_filter)) .where(Criterion.all(self.qb_selection_filter))
@ -731,6 +733,7 @@ class ReceivablePayableReport(object):
def prepare_conditions(self): def prepare_conditions(self):
self.qb_selection_filter = [] self.qb_selection_filter = []
party_type_field = scrub(self.party_type) party_type_field = scrub(self.party_type)
self.qb_selection_filter.append(self.ple.party_type == self.party_type)
self.add_common_filters(party_type_field=party_type_field) self.add_common_filters(party_type_field=party_type_field)
@ -974,6 +977,9 @@ class ReceivablePayableReport(object):
options="Supplier Group", options="Supplier Group",
) )
if self.filters.show_remarks:
self.add_column(label=_("Remarks"), fieldname="remarks", fieldtype="Text", width=200),
def add_column(self, label, fieldname=None, fieldtype="Currency", options=None, width=120): def add_column(self, label, fieldname=None, fieldtype="Currency", options=None, width=120):
if not fieldname: if not fieldname:
fieldname = scrub(label) fieldname = scrub(label)

View File

@ -535,7 +535,11 @@ def get_accounts(root_type, companies):
): ):
if account.account_name not in added_accounts: if account.account_name not in added_accounts:
accounts.append(account) accounts.append(account)
added_accounts.append(account.account_name) if account.account_number:
account_key = account.account_number + "-" + account.account_name
else:
account_key = account.account_name
added_accounts.append(account_key)
return accounts return accounts

View File

@ -1424,6 +1424,7 @@ def create_payment_ledger_entry(
"amount": dr_or_cr, "amount": dr_or_cr,
"amount_in_account_currency": dr_or_cr_account_currency, "amount_in_account_currency": dr_or_cr_account_currency,
"delinked": True if cancel else False, "delinked": True if cancel else False,
"remarks": gle.remarks,
} }
) )

View File

@ -1454,12 +1454,14 @@ def create_fixed_asset_item(item_code=None, auto_create_assets=1, is_grouped_ass
return item return item
def set_depreciation_settings_in_company(): def set_depreciation_settings_in_company(company=None):
company = frappe.get_doc("Company", "_Test Company") if not company:
company.accumulated_depreciation_account = "_Test Accumulated Depreciations - _TC" company = "_Test Company"
company.depreciation_expense_account = "_Test Depreciations - _TC" company = frappe.get_doc("Company", company)
company.disposal_account = "_Test Gain/Loss on Asset Disposal - _TC" company.accumulated_depreciation_account = "_Test Accumulated Depreciations - " + company.abbr
company.depreciation_cost_center = "_Test Cost Center - _TC" company.depreciation_expense_account = "_Test Depreciations - " + company.abbr
company.disposal_account = "_Test Gain/Loss on Asset Disposal - " + company.abbr
company.depreciation_cost_center = "Main - " + company.abbr
company.save() company.save()
# Enable booking asset depreciation entry automatically # Enable booking asset depreciation entry automatically

View File

@ -76,7 +76,7 @@ frappe.ui.form.on('Asset Repair Consumed Item', {
'warehouse': frm.doc.warehouse, 'warehouse': frm.doc.warehouse,
'qty': item.consumed_quantity, 'qty': item.consumed_quantity,
'serial_no': item.serial_no, 'serial_no': item.serial_no,
'company': frm.doc.company 'company': frm.doc.company,
}; };
frappe.call({ frappe.call({

View File

@ -238,7 +238,6 @@
"no_copy": 1 "no_copy": 1
}, },
{ {
"depends_on": "eval:!doc.__islocal",
"fieldname": "purchase_invoice", "fieldname": "purchase_invoice",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Purchase Invoice", "label": "Purchase Invoice",
@ -257,6 +256,7 @@
"fieldname": "stock_entry", "fieldname": "stock_entry",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Stock Entry", "label": "Stock Entry",
"no_copy": 1,
"options": "Stock Entry", "options": "Stock Entry",
"read_only": 1 "read_only": 1
} }
@ -264,10 +264,11 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-06-25 13:14:38.307723", "modified": "2022-08-16 15:55:25.023471",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Repair", "name": "Asset Repair",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@ -303,6 +304,7 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"title_field": "asset_name", "title_field": "asset_name",
"track_changes": 1, "track_changes": 1,
"track_seen": 1 "track_seen": 1

View File

@ -1,11 +1,11 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import add_months, cint, flt, getdate, time_diff_in_hours from frappe.utils import add_months, cint, flt, getdate, time_diff_in_hours
import erpnext
from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.assets.doctype.asset.asset import get_asset_account from erpnext.assets.doctype.asset.asset import get_asset_account
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
@ -17,7 +17,7 @@ class AssetRepair(AccountsController):
self.update_status() self.update_status()
if self.get("stock_items"): if self.get("stock_items"):
self.set_total_value() self.set_stock_items_cost()
self.calculate_total_repair_cost() self.calculate_total_repair_cost()
def update_status(self): def update_status(self):
@ -26,7 +26,7 @@ class AssetRepair(AccountsController):
else: else:
self.asset_doc.set_status() self.asset_doc.set_status()
def set_total_value(self): def set_stock_items_cost(self):
for item in self.get("stock_items"): for item in self.get("stock_items"):
item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity)
@ -66,6 +66,7 @@ class AssetRepair(AccountsController):
if self.get("capitalize_repair_cost"): if self.get("capitalize_repair_cost"):
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
self.make_gl_entries(cancel=True) self.make_gl_entries(cancel=True)
self.db_set("stock_entry", None)
if ( if (
frappe.db.get_value("Asset", self.asset, "calculate_depreciation") frappe.db.get_value("Asset", self.asset, "calculate_depreciation")
and self.increase_in_asset_life and self.increase_in_asset_life
@ -133,6 +134,7 @@ class AssetRepair(AccountsController):
"qty": stock_item.consumed_quantity, "qty": stock_item.consumed_quantity,
"basic_rate": stock_item.valuation_rate, "basic_rate": stock_item.valuation_rate,
"serial_no": stock_item.serial_no, "serial_no": stock_item.serial_no,
"cost_center": self.cost_center,
}, },
) )
@ -142,72 +144,42 @@ class AssetRepair(AccountsController):
self.db_set("stock_entry", stock_entry.name) self.db_set("stock_entry", stock_entry.name)
def increase_stock_quantity(self): def increase_stock_quantity(self):
stock_entry = frappe.get_doc("Stock Entry", self.stock_entry) if self.stock_entry:
stock_entry.flags.ignore_links = True stock_entry = frappe.get_doc("Stock Entry", self.stock_entry)
stock_entry.cancel() stock_entry.flags.ignore_links = True
stock_entry.cancel()
def make_gl_entries(self, cancel=False): def make_gl_entries(self, cancel=False):
if flt(self.repair_cost) > 0: if flt(self.total_repair_cost) > 0:
gl_entries = self.get_gl_entries() gl_entries = self.get_gl_entries()
make_gl_entries(gl_entries, cancel) make_gl_entries(gl_entries, cancel)
def get_gl_entries(self): def get_gl_entries(self):
gl_entries = [] gl_entries = []
repair_and_maintenance_account = frappe.db.get_value(
"Company", self.company, "repair_and_maintenance_account"
)
fixed_asset_account = get_asset_account( fixed_asset_account = get_asset_account(
"fixed_asset_account", asset=self.asset, company=self.company "fixed_asset_account", asset=self.asset, company=self.company
) )
expense_account = ( self.get_gl_entries_for_repair_cost(gl_entries, fixed_asset_account)
self.get_gl_entries_for_consumed_items(gl_entries, fixed_asset_account)
return gl_entries
def get_gl_entries_for_repair_cost(self, gl_entries, fixed_asset_account):
if flt(self.repair_cost) <= 0:
return
pi_expense_account = (
frappe.get_doc("Purchase Invoice", self.purchase_invoice).items[0].expense_account frappe.get_doc("Purchase Invoice", self.purchase_invoice).items[0].expense_account
) )
gl_entries.append(
self.get_gl_dict(
{
"account": expense_account,
"credit": self.repair_cost,
"credit_in_account_currency": self.repair_cost,
"against": repair_and_maintenance_account,
"voucher_type": self.doctype,
"voucher_no": self.name,
"cost_center": self.cost_center,
"posting_date": getdate(),
"company": self.company,
},
item=self,
)
)
if self.get("stock_consumption"):
# creating GL Entries for each row in Stock Items based on the Stock Entry created for it
stock_entry = frappe.get_doc("Stock Entry", self.stock_entry)
for item in stock_entry.items:
gl_entries.append(
self.get_gl_dict(
{
"account": item.expense_account,
"credit": item.amount,
"credit_in_account_currency": item.amount,
"against": repair_and_maintenance_account,
"voucher_type": self.doctype,
"voucher_no": self.name,
"cost_center": self.cost_center,
"posting_date": getdate(),
"company": self.company,
},
item=self,
)
)
gl_entries.append( gl_entries.append(
self.get_gl_dict( self.get_gl_dict(
{ {
"account": fixed_asset_account, "account": fixed_asset_account,
"debit": self.total_repair_cost, "debit": self.repair_cost,
"debit_in_account_currency": self.total_repair_cost, "debit_in_account_currency": self.repair_cost,
"against": expense_account, "against": pi_expense_account,
"voucher_type": self.doctype, "voucher_type": self.doctype,
"voucher_no": self.name, "voucher_no": self.name,
"cost_center": self.cost_center, "cost_center": self.cost_center,
@ -220,7 +192,75 @@ class AssetRepair(AccountsController):
) )
) )
return gl_entries gl_entries.append(
self.get_gl_dict(
{
"account": pi_expense_account,
"credit": self.repair_cost,
"credit_in_account_currency": self.repair_cost,
"against": fixed_asset_account,
"voucher_type": self.doctype,
"voucher_no": self.name,
"cost_center": self.cost_center,
"posting_date": getdate(),
"company": self.company,
},
item=self,
)
)
def get_gl_entries_for_consumed_items(self, gl_entries, fixed_asset_account):
if not (self.get("stock_consumption") and self.get("stock_items")):
return
# creating GL Entries for each row in Stock Items based on the Stock Entry created for it
stock_entry = frappe.get_doc("Stock Entry", self.stock_entry)
default_expense_account = None
if not erpnext.is_perpetual_inventory_enabled(self.company):
default_expense_account = frappe.get_cached_value(
"Company", self.company, "default_expense_account"
)
if not default_expense_account:
frappe.throw(_("Please set default Expense Account in Company {0}").format(self.company))
for item in stock_entry.items:
if flt(item.amount) > 0:
gl_entries.append(
self.get_gl_dict(
{
"account": item.expense_account or default_expense_account,
"credit": item.amount,
"credit_in_account_currency": item.amount,
"against": fixed_asset_account,
"voucher_type": self.doctype,
"voucher_no": self.name,
"cost_center": self.cost_center,
"posting_date": getdate(),
"company": self.company,
},
item=self,
)
)
gl_entries.append(
self.get_gl_dict(
{
"account": fixed_asset_account,
"debit": item.amount,
"debit_in_account_currency": item.amount,
"against": item.expense_account or default_expense_account,
"voucher_type": self.doctype,
"voucher_no": self.name,
"cost_center": self.cost_center,
"posting_date": getdate(),
"against_voucher_type": "Stock Entry",
"against_voucher": self.stock_entry,
"company": self.company,
},
item=self,
)
)
def modify_depreciation_schedule(self): def modify_depreciation_schedule(self):
for row in self.asset_doc.finance_books: for row in self.asset_doc.finance_books:

View File

@ -6,6 +6,7 @@ import unittest
import frappe import frappe
from frappe.utils import flt, nowdate from frappe.utils import flt, nowdate
from erpnext.assets.doctype.asset.asset import get_asset_account
from erpnext.assets.doctype.asset.test_asset import ( from erpnext.assets.doctype.asset.test_asset import (
create_asset, create_asset,
create_asset_data, create_asset_data,
@ -125,10 +126,109 @@ class TestAssetRepair(unittest.TestCase):
asset_repair = create_asset_repair(capitalize_repair_cost=1, submit=1) asset_repair = create_asset_repair(capitalize_repair_cost=1, submit=1)
self.assertTrue(asset_repair.purchase_invoice) self.assertTrue(asset_repair.purchase_invoice)
def test_gl_entries(self): def test_gl_entries_with_perpetual_inventory(self):
asset_repair = create_asset_repair(capitalize_repair_cost=1, submit=1) set_depreciation_settings_in_company(company="_Test Company with perpetual inventory")
gl_entry = frappe.get_last_doc("GL Entry")
self.assertEqual(asset_repair.name, gl_entry.voucher_no) asset_category = frappe.get_doc("Asset Category", "Computers")
asset_category.append(
"accounts",
{
"company_name": "_Test Company with perpetual inventory",
"fixed_asset_account": "_Test Fixed Asset - TCP1",
"accumulated_depreciation_account": "_Test Accumulated Depreciations - TCP1",
"depreciation_expense_account": "_Test Depreciations - TCP1",
},
)
asset_category.save()
asset_repair = create_asset_repair(
capitalize_repair_cost=1,
stock_consumption=1,
warehouse="Stores - TCP1",
company="_Test Company with perpetual inventory",
submit=1,
)
gl_entries = frappe.db.sql(
"""
select
account,
sum(debit) as debit,
sum(credit) as credit
from `tabGL Entry`
where
voucher_type='Asset Repair'
and voucher_no=%s
group by
account
""",
asset_repair.name,
as_dict=1,
)
self.assertTrue(gl_entries)
fixed_asset_account = get_asset_account(
"fixed_asset_account", asset=asset_repair.asset, company=asset_repair.company
)
pi_expense_account = (
frappe.get_doc("Purchase Invoice", asset_repair.purchase_invoice).items[0].expense_account
)
stock_entry_expense_account = (
frappe.get_doc("Stock Entry", asset_repair.stock_entry).get("items")[0].expense_account
)
expected_values = {
fixed_asset_account: [asset_repair.total_repair_cost, 0],
pi_expense_account: [0, asset_repair.repair_cost],
stock_entry_expense_account: [0, 100],
}
for d in gl_entries:
self.assertEqual(expected_values[d.account][0], d.debit)
self.assertEqual(expected_values[d.account][1], d.credit)
def test_gl_entries_with_periodical_inventory(self):
frappe.db.set_value(
"Company", "_Test Company", "default_expense_account", "Cost of Goods Sold - _TC"
)
asset_repair = create_asset_repair(
capitalize_repair_cost=1,
stock_consumption=1,
submit=1,
)
gl_entries = frappe.db.sql(
"""
select
account,
sum(debit) as debit,
sum(credit) as credit
from `tabGL Entry`
where
voucher_type='Asset Repair'
and voucher_no=%s
group by
account
""",
asset_repair.name,
as_dict=1,
)
self.assertTrue(gl_entries)
fixed_asset_account = get_asset_account(
"fixed_asset_account", asset=asset_repair.asset, company=asset_repair.company
)
default_expense_account = frappe.get_cached_value(
"Company", asset_repair.company, "default_expense_account"
)
expected_values = {fixed_asset_account: [1100, 0], default_expense_account: [0, 1100]}
for d in gl_entries:
self.assertEqual(expected_values[d.account][0], d.debit)
self.assertEqual(expected_values[d.account][1], d.credit)
def test_increase_in_asset_life(self): def test_increase_in_asset_life(self):
asset = create_asset(calculate_depreciation=1, submit=1) asset = create_asset(calculate_depreciation=1, submit=1)
@ -160,7 +260,7 @@ def create_asset_repair(**args):
if args.asset: if args.asset:
asset = args.asset asset = args.asset
else: else:
asset = create_asset(is_existing_asset=1, submit=1) asset = create_asset(is_existing_asset=1, submit=1, company=args.company)
asset_repair = frappe.new_doc("Asset Repair") asset_repair = frappe.new_doc("Asset Repair")
asset_repair.update( asset_repair.update(
{ {
@ -192,7 +292,7 @@ def create_asset_repair(**args):
if args.submit: if args.submit:
asset_repair.repair_status = "Completed" asset_repair.repair_status = "Completed"
asset_repair.cost_center = "_Test Cost Center - _TC" asset_repair.cost_center = frappe.db.get_value("Company", asset.company, "cost_center")
if args.stock_consumption: if args.stock_consumption:
stock_entry = frappe.get_doc( stock_entry = frappe.get_doc(
@ -204,6 +304,8 @@ def create_asset_repair(**args):
"t_warehouse": asset_repair.warehouse, "t_warehouse": asset_repair.warehouse,
"item_code": asset_repair.stock_items[0].item_code, "item_code": asset_repair.stock_items[0].item_code,
"qty": asset_repair.stock_items[0].consumed_quantity, "qty": asset_repair.stock_items[0].consumed_quantity,
"basic_rate": args.rate if args.get("rate") is not None else 100,
"cost_center": asset_repair.cost_center,
}, },
) )
stock_entry.submit() stock_entry.submit()
@ -213,7 +315,13 @@ def create_asset_repair(**args):
asset_repair.repair_cost = 1000 asset_repair.repair_cost = 1000
if asset.calculate_depreciation: if asset.calculate_depreciation:
asset_repair.increase_in_asset_life = 12 asset_repair.increase_in_asset_life = 12
asset_repair.purchase_invoice = make_purchase_invoice().name pi = make_purchase_invoice(
company=asset.company,
expense_account=frappe.db.get_value("Company", asset.company, "default_expense_account"),
cost_center=asset_repair.cost_center,
warehouse=asset_repair.warehouse,
)
asset_repair.purchase_invoice = pi.name
asset_repair.submit() asset_repair.submit()
return asset_repair return asset_repair

View File

@ -76,7 +76,7 @@
"label": "Subcontracting Settings" "label": "Subcontracting Settings"
}, },
{ {
"default": "Material Transferred for Subcontract", "default": "BOM",
"fieldname": "backflush_raw_materials_of_subcontract_based_on", "fieldname": "backflush_raw_materials_of_subcontract_based_on",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Backflush Raw Materials of Subcontract Based On", "label": "Backflush Raw Materials of Subcontract Based On",
@ -148,7 +148,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2022-05-31 19:40:26.103909", "modified": "2022-09-01 18:01:34.994657",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying Settings", "name": "Buying Settings",

View File

@ -41,6 +41,7 @@ class calculate_taxes_and_totals(object):
if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"): if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"):
self.doc.grand_total -= self.doc.discount_amount self.doc.grand_total -= self.doc.discount_amount
self.doc.base_grand_total -= self.doc.base_discount_amount self.doc.base_grand_total -= self.doc.base_discount_amount
self.set_rounded_total()
self.calculate_shipping_charges() self.calculate_shipping_charges()

View File

@ -200,7 +200,7 @@ erpnext.ProductSearch = class {
let thumbnail = res.thumbnail || '/assets/erpnext/images/ui-states/cart-empty-state.png'; let thumbnail = res.thumbnail || '/assets/erpnext/images/ui-states/cart-empty-state.png';
html += ` html += `
<div class="dropdown-item" style="display: flex;"> <div class="dropdown-item" style="display: flex;">
<img class="item-thumb col-2" src=${thumbnail} /> <img class="item-thumb col-2" src=${encodeURI(thumbnail)} />
<div class="col-9" style="white-space: normal;"> <div class="col-9" style="white-space: normal;">
<a href="/${res.route}">${res.web_item_name}</a><br> <a href="/${res.route}">${res.web_item_name}</a><br>
<span class="brand-line">${res.brand ? "by " + res.brand : ""}</span> <span class="brand-line">${res.brand ? "by " + res.brand : ""}</span>

View File

@ -7,7 +7,9 @@ import frappe
from frappe import _ from frappe import _
from frappe.utils.redis_wrapper import RedisWrapper from frappe.utils.redis_wrapper import RedisWrapper
from redis import ResponseError from redis import ResponseError
from redisearch import AutoCompleter, Client, IndexDefinition, Suggestion, TagField, TextField from redis.commands.search.field import TagField, TextField
from redis.commands.search.indexDefinition import IndexDefinition
from redis.commands.search.suggestion import Suggestion
WEBSITE_ITEM_INDEX = "website_items_index" WEBSITE_ITEM_INDEX = "website_items_index"
WEBSITE_ITEM_KEY_PREFIX = "website_item:" WEBSITE_ITEM_KEY_PREFIX = "website_item:"
@ -35,12 +37,9 @@ def is_redisearch_enabled():
def is_search_module_loaded(): def is_search_module_loaded():
try: try:
cache = frappe.cache() cache = frappe.cache()
out = cache.execute_command("MODULE LIST") for module in cache.module_list():
if module.get(b"name") == b"search":
parsed_output = " ".join( return True
(" ".join([frappe.as_unicode(s) for s in o if not isinstance(s, int)]) for o in out)
)
return "search" in parsed_output
except Exception: except Exception:
return False # handling older redis versions return False # handling older redis versions
@ -58,18 +57,18 @@ def if_redisearch_enabled(function):
def make_key(key): def make_key(key):
return "{0}|{1}".format(frappe.conf.db_name, key).encode("utf-8") return frappe.cache().make_key(key)
@if_redisearch_enabled @if_redisearch_enabled
def create_website_items_index(): def create_website_items_index():
"Creates Index Definition." "Creates Index Definition."
# CREATE index redis = frappe.cache()
client = Client(make_key(WEBSITE_ITEM_INDEX), conn=frappe.cache()) index = redis.ft(WEBSITE_ITEM_INDEX)
try: try:
client.drop_index() # drop if already exists index.dropindex() # drop if already exists
except ResponseError: except ResponseError:
# will most likely raise a ResponseError if index does not exist # will most likely raise a ResponseError if index does not exist
# ignore and create index # ignore and create index
@ -86,9 +85,10 @@ def create_website_items_index():
if "web_item_name" in idx_fields: if "web_item_name" in idx_fields:
idx_fields.remove("web_item_name") idx_fields.remove("web_item_name")
idx_fields = list(map(to_search_field, idx_fields)) idx_fields = [to_search_field(f) for f in idx_fields]
client.create_index( # TODO: sortable?
index.create_index(
[TextField("web_item_name", sortable=True)] + idx_fields, [TextField("web_item_name", sortable=True)] + idx_fields,
definition=idx_def, definition=idx_def,
) )
@ -119,8 +119,8 @@ def insert_item_to_index(website_item_doc):
@if_redisearch_enabled @if_redisearch_enabled
def insert_to_name_ac(web_name, doc_name): def insert_to_name_ac(web_name, doc_name):
ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=frappe.cache()) ac = frappe.cache().ft()
ac.add_suggestions(Suggestion(web_name, payload=doc_name)) ac.sugadd(WEBSITE_ITEM_NAME_AUTOCOMPLETE, Suggestion(web_name, payload=doc_name))
def create_web_item_map(website_item_doc): def create_web_item_map(website_item_doc):
@ -157,9 +157,8 @@ def delete_item_from_index(website_item_doc):
@if_redisearch_enabled @if_redisearch_enabled
def delete_from_ac_dict(website_item_doc): def delete_from_ac_dict(website_item_doc):
"""Removes this items's name from autocomplete dictionary""" """Removes this items's name from autocomplete dictionary"""
cache = frappe.cache() ac = frappe.cache().ft()
name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache) ac.sugdel(website_item_doc.web_item_name)
name_ac.delete(website_item_doc.web_item_name)
@if_redisearch_enabled @if_redisearch_enabled
@ -170,8 +169,6 @@ def define_autocomplete_dictionary():
""" """
cache = frappe.cache() cache = frappe.cache()
item_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache)
item_group_ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=cache)
# Delete both autocomplete dicts # Delete both autocomplete dicts
try: try:
@ -180,38 +177,43 @@ def define_autocomplete_dictionary():
except Exception: except Exception:
raise_redisearch_error() raise_redisearch_error()
create_items_autocomplete_dict(autocompleter=item_ac) create_items_autocomplete_dict()
create_item_groups_autocomplete_dict(autocompleter=item_group_ac) create_item_groups_autocomplete_dict()
@if_redisearch_enabled @if_redisearch_enabled
def create_items_autocomplete_dict(autocompleter): def create_items_autocomplete_dict():
"Add items as suggestions in Autocompleter." "Add items as suggestions in Autocompleter."
ac = frappe.cache().ft()
items = frappe.get_all( items = frappe.get_all(
"Website Item", fields=["web_item_name", "item_group"], filters={"published": 1} "Website Item", fields=["web_item_name", "item_group"], filters={"published": 1}
) )
for item in items: for item in items:
autocompleter.add_suggestions(Suggestion(item.web_item_name)) ac.sugadd(WEBSITE_ITEM_NAME_AUTOCOMPLETE, Suggestion(item.web_item_name))
@if_redisearch_enabled @if_redisearch_enabled
def create_item_groups_autocomplete_dict(autocompleter): def create_item_groups_autocomplete_dict():
"Add item groups with weightage as suggestions in Autocompleter." "Add item groups with weightage as suggestions in Autocompleter."
published_item_groups = frappe.get_all( published_item_groups = frappe.get_all(
"Item Group", fields=["name", "route", "weightage"], filters={"show_in_website": 1} "Item Group", fields=["name", "route", "weightage"], filters={"show_in_website": 1}
) )
if not published_item_groups: if not published_item_groups:
return return
ac = frappe.cache().ft()
for item_group in published_item_groups: for item_group in published_item_groups:
payload = json.dumps({"name": item_group.name, "route": item_group.route}) payload = json.dumps({"name": item_group.name, "route": item_group.route})
autocompleter.add_suggestions( ac.sugadd(
WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE,
Suggestion( Suggestion(
string=item_group.name, string=item_group.name,
score=frappe.utils.flt(item_group.weightage) or 1.0, score=frappe.utils.flt(item_group.weightage) or 1.0,
payload=payload, # additional info that can be retrieved later payload=payload, # additional info that can be retrieved later
) ),
) )

View File

@ -236,7 +236,7 @@ def get_term_loans(date, term_loan=None, loan_type=None):
AND l.is_term_loan =1 AND l.is_term_loan =1
AND rs.payment_date <= %s AND rs.payment_date <= %s
AND rs.is_accrued=0 {0} AND rs.is_accrued=0 {0}
AND rs.interest_amount > 0 AND rs.principal_amount > 0
AND l.status = 'Disbursed' AND l.status = 'Disbursed'
ORDER BY rs.payment_date""".format( ORDER BY rs.payment_date""".format(
condition condition

View File

@ -735,6 +735,7 @@ def get_amounts(amounts, against_loan, posting_date):
) )
amounts["pending_accrual_entries"] = pending_accrual_entries amounts["pending_accrual_entries"] = pending_accrual_entries
amounts["unaccrued_interest"] = flt(unaccrued_interest, precision) amounts["unaccrued_interest"] = flt(unaccrued_interest, precision)
amounts["written_off_amount"] = flt(against_loan_doc.written_off_amount, precision)
if final_due_date: if final_due_date:
amounts["due_date"] = final_due_date amounts["due_date"] = final_due_date

View File

@ -57,7 +57,7 @@ def process_loan_interest_accrual_for_demand_loans(
def process_loan_interest_accrual_for_term_loans(posting_date=None, loan_type=None, loan=None): def process_loan_interest_accrual_for_term_loans(posting_date=None, loan_type=None, loan=None):
if not term_loan_accrual_pending(posting_date or nowdate()): if not term_loan_accrual_pending(posting_date or nowdate(), loan=loan):
return return
loan_process = frappe.new_doc("Process Loan Interest Accrual") loan_process = frappe.new_doc("Process Loan Interest Accrual")
@ -71,9 +71,12 @@ def process_loan_interest_accrual_for_term_loans(posting_date=None, loan_type=No
return loan_process.name return loan_process.name
def term_loan_accrual_pending(date): def term_loan_accrual_pending(date, loan=None):
pending_accrual = frappe.db.get_value( filters = {"payment_date": ("<=", date), "is_accrued": 0}
"Repayment Schedule", {"payment_date": ("<=", date), "is_accrued": 0}
) if loan:
filters.update({"parent": loan})
pending_accrual = frappe.db.get_value("Repayment Schedule", filters)
return pending_accrual return pending_accrual

View File

@ -656,6 +656,8 @@ class ProductionPlan(Document):
row.idx = idx + 1 row.idx = idx + 1
self.append("sub_assembly_items", row) self.append("sub_assembly_items", row)
self.set_default_supplier_for_subcontracting_order()
def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None): def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None):
"Modify bom_data, set additional details." "Modify bom_data, set additional details."
for data in bom_data: for data in bom_data:
@ -667,6 +669,32 @@ class ProductionPlan(Document):
"Subcontract" if data.is_sub_contracted_item else "In House" "Subcontract" if data.is_sub_contracted_item else "In House"
) )
def set_default_supplier_for_subcontracting_order(self):
items = [
d.production_item for d in self.sub_assembly_items if d.type_of_manufacturing == "Subcontract"
]
if not items:
return
default_supplier = frappe._dict(
frappe.get_all(
"Item Default",
fields=["parent", "default_supplier"],
filters={"parent": ("in", items), "default_supplier": ("is", "set")},
as_list=1,
)
)
if not default_supplier:
return
for row in self.sub_assembly_items:
if row.type_of_manufacturing != "Subcontract":
continue
row.supplier = default_supplier.get(row.production_item)
def combine_subassembly_items(self, sub_assembly_items_store): def combine_subassembly_items(self, sub_assembly_items_store):
"Aggregate if same: Item, Warehouse, Inhouse/Outhouse Manu.g, BOM No." "Aggregate if same: Item, Warehouse, Inhouse/Outhouse Manu.g, BOM No."
key_wise_data = {} key_wise_data = {}

View File

@ -281,6 +281,31 @@ class TestProductionPlan(FrappeTestCase):
pln.reload() pln.reload()
pln.cancel() pln.cancel()
def test_production_plan_subassembly_default_supplier(self):
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
bom_tree_1 = {"Test Laptop": {"Test Motherboard": {"Test Motherboard Wires": {}}}}
bom = create_nested_bom(bom_tree_1, prefix="")
item_doc = frappe.get_doc("Item", "Test Motherboard")
company = "_Test Company"
item_doc.is_sub_contracted_item = 1
for row in item_doc.item_defaults:
if row.company == company and not row.default_supplier:
row.default_supplier = "_Test Supplier"
if not item_doc.item_defaults:
item_doc.append("item_defaults", {"company": company, "default_supplier": "_Test Supplier"})
item_doc.save()
plan = create_production_plan(item_code="Test Laptop", use_multi_level_bom=1, do_not_submit=True)
plan.get_sub_assembly_items()
plan.set_default_supplier_for_subcontracting_order()
self.assertEqual(plan.sub_assembly_items[0].supplier, "_Test Supplier")
def test_production_plan_combine_subassembly(self): def test_production_plan_combine_subassembly(self):
""" """
Test combining Sub assembly items belonging to the same BOM in Prod Plan. Test combining Sub assembly items belonging to the same BOM in Prod Plan.

View File

@ -7,6 +7,6 @@ def get_data():
"non_standard_fieldnames": {"Batch": "reference_name"}, "non_standard_fieldnames": {"Batch": "reference_name"},
"transactions": [ "transactions": [
{"label": _("Transactions"), "items": ["Stock Entry", "Job Card", "Pick List"]}, {"label": _("Transactions"), "items": ["Stock Entry", "Job Card", "Pick List"]},
{"label": _("Reference"), "items": ["Serial No", "Batch"]}, {"label": _("Reference"), "items": ["Serial No", "Batch", "Material Request"]},
], ],
} }

View File

@ -312,3 +312,4 @@ erpnext.patches.v13_0.fix_number_and_frequency_for_monthly_depreciation
erpnext.patches.v14_0.remove_hr_and_payroll_modules # 20-07-2022 erpnext.patches.v14_0.remove_hr_and_payroll_modules # 20-07-2022
erpnext.patches.v14_0.fix_crm_no_of_employees erpnext.patches.v14_0.fix_crm_no_of_employees
erpnext.patches.v14_0.create_accounting_dimensions_in_subcontracting_doctypes erpnext.patches.v14_0.create_accounting_dimensions_in_subcontracting_doctypes
erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger

View File

@ -14,7 +14,8 @@ def execute():
for sla in frappe.get_all("Service Level Agreement"): for sla in frappe.get_all("Service Level Agreement"):
agreement = frappe.get_doc("Service Level Agreement", sla.name) agreement = frappe.get_doc("Service Level Agreement", sla.name)
agreement.document_type = "Issue" agreement.db_set("document_type", "Issue")
agreement.reload()
agreement.apply_sla_for_resolution = 1 agreement.apply_sla_for_resolution = 1
agreement.append("sla_fulfilled_on", {"status": "Resolved"}) agreement.append("sla_fulfilled_on", {"status": "Resolved"})
agreement.append("sla_fulfilled_on", {"status": "Closed"}) agreement.append("sla_fulfilled_on", {"status": "Closed"})

View File

@ -16,18 +16,18 @@ def execute():
delete_auto_email_reports(report) delete_auto_email_reports(report)
check_and_delete_linked_reports(report) check_and_delete_linked_reports(report)
frappe.delete_doc("Report", report) frappe.delete_doc("Report", report, force=True)
def delete_auto_email_reports(report): def delete_auto_email_reports(report):
"""Check for one or multiple Auto Email Reports and delete""" """Check for one or multiple Auto Email Reports and delete"""
auto_email_reports = frappe.db.get_values("Auto Email Report", {"report": report}, ["name"]) auto_email_reports = frappe.db.get_values("Auto Email Report", {"report": report}, ["name"])
for auto_email_report in auto_email_reports: for auto_email_report in auto_email_reports:
frappe.delete_doc("Auto Email Report", auto_email_report[0]) frappe.delete_doc("Auto Email Report", auto_email_report[0], force=True)
def delete_links_from_desktop_icons(report): def delete_links_from_desktop_icons(report):
"""Check for one or multiple Desktop Icons and delete""" """Check for one or multiple Desktop Icons and delete"""
desktop_icons = frappe.db.get_values("Desktop Icon", {"_report": report}, ["name"]) desktop_icons = frappe.db.get_values("Desktop Icon", {"_report": report}, ["name"])
for desktop_icon in desktop_icons: for desktop_icon in desktop_icons:
frappe.delete_doc("Desktop Icon", desktop_icon[0]) frappe.delete_doc("Desktop Icon", desktop_icon[0], force=True)

View File

@ -0,0 +1,56 @@
import frappe
from frappe import qb
from frappe.utils import create_batch
def execute():
if frappe.reload_doc("accounts", "doctype", "payment_ledger_entry"):
gle = qb.DocType("GL Entry")
ple = qb.DocType("Payment Ledger Entry")
# get ple and their remarks from GL Entry
pl_entries = (
qb.from_(ple)
.left_join(gle)
.on(
(ple.account == gle.account)
& (ple.party_type == gle.party_type)
& (ple.party == gle.party)
& (ple.voucher_type == gle.voucher_type)
& (ple.voucher_no == gle.voucher_no)
& (ple.company == gle.company)
)
.select(
ple.company,
ple.account,
ple.party_type,
ple.party,
ple.voucher_type,
ple.voucher_no,
gle.remarks.as_("gle_remarks"),
)
.where((ple.delinked == 0) & (gle.is_cancelled == 0))
.run(as_dict=True)
)
if pl_entries:
# split into multiple batches, update and commit for each batch
batch_size = 1000
for batch in create_batch(pl_entries, batch_size):
for entry in batch:
query = (
qb.update(ple)
.set(ple.remarks, entry.gle_remarks)
.where(
(ple.company == entry.company)
& (ple.account == entry.account)
& (ple.party_type == entry.party_type)
& (ple.party == entry.party)
& (ple.voucher_type == entry.voucher_type)
& (ple.voucher_no == entry.voucher_no)
)
)
query.run()
frappe.db.commit()

View File

@ -1,127 +1,70 @@
{ {
"allow_copy": 0, "actions": [],
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "Prompt", "autoname": "Prompt",
"beta": 0,
"creation": "2019-04-19 15:04:05.317138", "creation": "2019-04-19 15:04:05.317138",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "",
"editable_grid": 0,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"weight",
"description"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "weight", "fieldname": "weight",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0, "label": "Weight"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Weight",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "description", "fieldname": "description",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"hidden": 0, "label": "Description"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Description",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0, "links": [],
"hide_toolbar": 0, "modified": "2022-08-29 17:46:41.342979",
"idx": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-04-19 15:31:48.080164",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Projects", "module": "Projects",
"name": "Task Type", "name": "Task Type",
"name_case": "", "naming_rule": "Set by user",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "System Manager", "role": "System Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Projects Manager",
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Projects User",
"share": 1
} }
], ],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"track_changes": 1, "states": [],
"track_seen": 0, "track_changes": 1
"track_views": 0
} }

View File

@ -268,7 +268,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
def set_expired_status(): def set_expired_status():
# filter out submitted non expired quotations whose validity has been ended # filter out submitted non expired quotations whose validity has been ended
cond = "`tabQuotation`.docstatus = 1 and `tabQuotation`.status != 'Expired' and `tabQuotation`.valid_till < %s" cond = "`tabQuotation`.docstatus = 1 and `tabQuotation`.status NOT IN ('Expired', 'Lost') and `tabQuotation`.valid_till < %s"
# check if those QUO have SO against it # check if those QUO have SO against it
so_against_quo = """ so_against_quo = """
SELECT SELECT

View File

@ -892,6 +892,7 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
target.additional_discount_percentage = 0.0 target.additional_discount_percentage = 0.0
target.discount_amount = 0.0 target.discount_amount = 0.0
target.inter_company_order_reference = "" target.inter_company_order_reference = ""
target.shipping_rule = ""
default_price_list = frappe.get_value("Supplier", supplier, "default_price_list") default_price_list = frappe.get_value("Supplier", supplier, "default_price_list")
if default_price_list: if default_price_list:
@ -1010,6 +1011,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
target.additional_discount_percentage = 0.0 target.additional_discount_percentage = 0.0
target.discount_amount = 0.0 target.discount_amount = 0.0
target.inter_company_order_reference = "" target.inter_company_order_reference = ""
target.shipping_rule = ""
target.customer = "" target.customer = ""
target.customer_name = "" target.customer_name = ""
target.run_method("set_missing_values") target.run_method("set_missing_values")

View File

@ -85,7 +85,6 @@
"depreciation_expense_account", "depreciation_expense_account",
"series_for_depreciation_entry", "series_for_depreciation_entry",
"expenses_included_in_asset_valuation", "expenses_included_in_asset_valuation",
"repair_and_maintenance_account",
"column_break_40", "column_break_40",
"disposal_account", "disposal_account",
"depreciation_cost_center", "depreciation_cost_center",
@ -234,7 +233,6 @@
"label": "Default Warehouse for Sales Return", "label": "Default Warehouse for Sales Return",
"options": "Warehouse" "options": "Warehouse"
}, },
{ {
"fieldname": "country", "fieldname": "country",
"fieldtype": "Link", "fieldtype": "Link",
@ -678,12 +676,6 @@
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Fixed Asset Defaults" "label": "Fixed Asset Defaults"
}, },
{
"fieldname": "repair_and_maintenance_account",
"fieldtype": "Link",
"label": "Repair and Maintenance Account",
"options": "Account"
},
{ {
"fieldname": "section_break_28", "fieldname": "section_break_28",
"fieldtype": "Section Break", "fieldtype": "Section Break",
@ -709,7 +701,7 @@
"image_field": "company_logo", "image_field": "company_logo",
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"modified": "2022-06-30 18:03:18.701314", "modified": "2022-08-16 16:09:02.327724",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Company", "name": "Company",

View File

@ -562,7 +562,7 @@ $.extend(erpnext.item, {
let selected_attributes = {}; let selected_attributes = {};
me.multiple_variant_dialog.$wrapper.find('.form-column').each((i, col) => { me.multiple_variant_dialog.$wrapper.find('.form-column').each((i, col) => {
if(i===0) return; if(i===0) return;
let attribute_name = $(col).find('label').html().trim(); let attribute_name = $(col).find('.control-label').html().trim();
selected_attributes[attribute_name] = []; selected_attributes[attribute_name] = [];
let checked_opts = $(col).find('.checkbox input'); let checked_opts = $(col).find('.checkbox input');
checked_opts.each((i, opt) => { checked_opts.each((i, opt) => {

View File

@ -37,7 +37,8 @@
"tc_name", "tc_name",
"terms", "terms",
"reference", "reference",
"job_card" "job_card",
"work_order"
], ],
"fields": [ "fields": [
{ {
@ -309,16 +310,24 @@
"label": "Transfer Status", "label": "Transfer Status",
"options": "\nNot Started\nIn Transit\nCompleted", "options": "\nNot Started\nIn Transit\nCompleted",
"read_only": 1 "read_only": 1
},
{
"fieldname": "work_order",
"fieldtype": "Link",
"label": "Work Order",
"options": "Work Order",
"read_only": 1
} }
], ],
"icon": "fa fa-ticket", "icon": "fa fa-ticket",
"idx": 70, "idx": 70,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-08-17 20:16:12.737743", "modified": "2022-08-25 11:49:28.155048",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Material Request", "name": "Material Request",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@ -386,5 +395,6 @@
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"title_field": "title" "title_field": "title"
} }

View File

@ -174,6 +174,8 @@ frappe.ui.form.on('Stock Entry', {
if(!items.length) { if(!items.length) {
items = frm.doc.items; items = frm.doc.items;
} }
mr.work_order = frm.doc.work_order;
items.forEach(function(item) { items.forEach(function(item) {
var mr_item = frappe.model.add_child(mr, 'items'); var mr_item = frappe.model.add_child(mr, 'items');
mr_item.item_code = item.item_code; mr_item.item_code = item.item_code;

View File

@ -116,6 +116,7 @@ class StockEntry(StockController):
self.validate_warehouse() self.validate_warehouse()
self.validate_work_order() self.validate_work_order()
self.validate_bom() self.validate_bom()
self.validate_purchase_order()
if self.purpose in ("Manufacture", "Repack"): if self.purpose in ("Manufacture", "Repack"):
self.mark_finished_and_scrap_items() self.mark_finished_and_scrap_items()
@ -946,6 +947,19 @@ class StockEntry(StockController):
item_code = d.original_item or d.item_code item_code = d.original_item or d.item_code
validate_bom_no(item_code, d.bom_no) validate_bom_no(item_code, d.bom_no)
def validate_purchase_order(self):
if self.purpose == "Send to Subcontractor" and self.get("purchase_order"):
is_old_subcontracting_flow = frappe.db.get_value(
"Purchase Order", self.purchase_order, "is_old_subcontracting_flow"
)
if not is_old_subcontracting_flow:
frappe.throw(
_("Please select Subcontracting Order instead of Purchase Order {0}").format(
self.purchase_order
)
)
def mark_finished_and_scrap_items(self): def mark_finished_and_scrap_items(self):
if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]): if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]):
return return
@ -2215,7 +2229,7 @@ class StockEntry(StockController):
return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos))) return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos)))
def update_subcontracting_order_status(self): def update_subcontracting_order_status(self):
if self.subcontracting_order and self.purpose == "Send to Subcontractor": if self.subcontracting_order and self.purpose in ["Send to Subcontractor", "Material Transfer"]:
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
update_subcontracting_order_status, update_subcontracting_order_status,
) )
@ -2554,27 +2568,26 @@ def get_supplied_items(
@frappe.whitelist() @frappe.whitelist()
def get_items_from_subcontracting_order(source_name, target_doc=None): def get_items_from_subcontracting_order(source_name, target_doc=None):
sco = frappe.get_doc("Subcontracting Order", source_name) def post_process(source, target):
target.stock_entry_type = target.purpose = "Send to Subcontractor"
target.subcontracting_order = source_name
if sco.docstatus == 1: if target.items:
if target_doc and isinstance(target_doc, str): target.items = []
target_doc = frappe.get_doc(json.loads(target_doc))
if target_doc.items:
target_doc.items = []
warehouses = {} warehouses = {}
for item in sco.items: for item in source.items:
warehouses[item.name] = item.warehouse warehouses[item.name] = item.warehouse
for item in sco.supplied_items: for item in source.supplied_items:
target_doc.append( target.append(
"items", "items",
{ {
"s_warehouse": warehouses.get(item.reference_name), "s_warehouse": warehouses.get(item.reference_name),
"t_warehouse": sco.supplier_warehouse, "t_warehouse": source.supplier_warehouse,
"subcontracted_item": item.main_item_code,
"item_code": item.rm_item_code, "item_code": item.rm_item_code,
"qty": item.required_qty, "qty": max(item.required_qty - item.total_supplied_qty, 0),
"transfer_qty": item.required_qty, "transfer_qty": item.required_qty,
"uom": item.stock_uom, "uom": item.stock_uom,
"stock_uom": item.stock_uom, "stock_uom": item.stock_uom,
@ -2582,6 +2595,23 @@ def get_items_from_subcontracting_order(source_name, target_doc=None):
}, },
) )
target_doc = get_mapped_doc(
"Subcontracting Order",
source_name,
{
"Subcontracting Order": {
"doctype": "Stock Entry",
"field_no_map": ["purchase_order"],
"validation": {
"docstatus": ["=", 1],
},
},
},
target_doc,
post_process,
ignore_child_tables=True,
)
return target_doc return target_doc

View File

@ -107,9 +107,9 @@ frappe.ui.form.on('Subcontracting Order', {
get_materials_from_supplier: function (frm) { get_materials_from_supplier: function (frm) {
let sco_rm_details = []; let sco_rm_details = [];
if (frm.doc.supplied_items && (frm.doc.per_received == 100)) { if (frm.doc.supplied_items && frm.doc.per_received > 0) {
frm.doc.supplied_items.forEach(d => { frm.doc.supplied_items.forEach(d => {
if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) { if (d.total_supplied_qty > 0 && d.total_supplied_qty != d.consumed_qty) {
sco_rm_details.push(d.name); sco_rm_details.push(d.name);
} }
}); });
@ -160,14 +160,11 @@ erpnext.buying.SubcontractingOrderController = class SubcontractingOrderControll
var me = this; var me = this;
if (doc.docstatus == 1) { if (doc.docstatus == 1) {
if (doc.status != 'Completed') { if (!['Closed', 'Completed'].includes(doc.status)) {
if (flt(doc.per_received) < 100) { if (flt(doc.per_received) < 100) {
cur_frm.add_custom_button(__('Subcontracting Receipt'), this.make_subcontracting_receipt, __('Create')); cur_frm.add_custom_button(__('Subcontracting Receipt'), this.make_subcontracting_receipt, __('Create'));
if (me.has_unsupplied_items()) { if (me.has_unsupplied_items()) {
cur_frm.add_custom_button(__('Material to Supplier'), cur_frm.add_custom_button(__('Material to Supplier'), this.make_stock_entry, __('Transfer'));
() => {
me.make_stock_entry();
}, __('Transfer'));
} }
} }
cur_frm.page.set_inner_btn_group_as_primary(__('Create')); cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
@ -195,120 +192,6 @@ erpnext.buying.SubcontractingOrderController = class SubcontractingOrderControll
transaction_controller.autofill_warehouse(child_table, warehouse_field, warehouse); transaction_controller.autofill_warehouse(child_table, warehouse_field, warehouse);
} }
make_stock_entry() {
var items = $.map(cur_frm.doc.items, (d) => d.bom ? d.item_code : false);
var me = this;
if (items.length >= 1) {
me.raw_material_data = [];
me.show_dialog = 1;
let title = __('Transfer Material to Supplier');
let fields = [
{ fieldtype: 'Section Break', label: __('Raw Materials') },
{
fieldname: 'sub_con_rm_items', fieldtype: 'Table', label: __('Items'),
fields: [
{
fieldtype: 'Data',
fieldname: 'item_code',
label: __('Item'),
read_only: 1,
in_list_view: 1
},
{
fieldtype: 'Data',
fieldname: 'rm_item_code',
label: __('Raw Material'),
read_only: 1,
in_list_view: 1
},
{
fieldtype: 'Float',
read_only: 1,
fieldname: 'qty',
label: __('Quantity'),
in_list_view: 1
},
{
fieldtype: 'Data',
read_only: 1,
fieldname: 'warehouse',
label: __('Reserve Warehouse'),
in_list_view: 1
},
{
fieldtype: 'Float',
read_only: 1,
fieldname: 'rate',
label: __('Rate'),
hidden: 1
},
{
fieldtype: 'Float',
read_only: 1,
fieldname: 'amount',
label: __('Amount'),
hidden: 1
},
{
fieldtype: 'Link',
read_only: 1,
fieldname: 'uom',
label: __('UOM'),
hidden: 1
}
],
data: me.raw_material_data,
get_data: () => me.raw_material_data
}
];
me.dialog = new frappe.ui.Dialog({
title: title, fields: fields
});
if (me.frm.doc['supplied_items']) {
me.frm.doc['supplied_items'].forEach((item) => {
if (item.rm_item_code && item.main_item_code && item.required_qty - item.supplied_qty != 0) {
me.raw_material_data.push({
'name': item.name,
'item_code': item.main_item_code,
'rm_item_code': item.rm_item_code,
'item_name': item.rm_item_code,
'qty': item.required_qty - item.supplied_qty,
'warehouse': item.reserve_warehouse,
'rate': item.rate,
'amount': item.amount,
'stock_uom': item.stock_uom
});
me.dialog.fields_dict.sub_con_rm_items.grid.refresh();
}
});
}
me.dialog.get_field('sub_con_rm_items').check_all_rows();
me.dialog.show();
this.dialog.set_primary_action(__('Transfer'), () => {
me.values = me.dialog.get_values();
if (me.values) {
me.values.sub_con_rm_items.map((row, i) => {
if (!row.item_code || !row.rm_item_code || !row.warehouse || !row.qty || row.qty === 0) {
let row_id = i + 1;
frappe.throw(__('Item Code, warehouse and quantity are required on row {0}', [row_id]));
}
});
me.make_rm_stock_entry(me.dialog.fields_dict.sub_con_rm_items.grid.get_selected_children());
me.dialog.hide();
}
});
}
me.dialog.get_close_btn().on('click', () => {
me.dialog.hide();
});
}
has_unsupplied_items() { has_unsupplied_items() {
return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty); return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty);
} }
@ -321,6 +204,15 @@ erpnext.buying.SubcontractingOrderController = class SubcontractingOrderControll
}); });
} }
make_stock_entry() {
frappe.model.open_mapped_doc({
method: 'erpnext.stock.doctype.stock_entry.stock_entry.get_items_from_subcontracting_order',
source_name: cur_frm.doc.name,
freeze: true,
freeze_message: __('Creating Stock Entry ...')
});
}
make_rm_stock_entry(rm_items) { make_rm_stock_entry(rm_items) {
frappe.call({ frappe.call({
method: 'erpnext.controllers.subcontracting_controller.make_rm_stock_entry', method: 'erpnext.controllers.subcontracting_controller.make_rm_stock_entry',

View File

@ -153,7 +153,7 @@ class SubcontractingOrder(SubcontractingController):
else: else:
self.set_missing_values() self.set_missing_values()
def update_status(self, status=None, update_modified=False): def update_status(self, status=None, update_modified=True):
if self.docstatus >= 1 and not status: if self.docstatus >= 1 and not status:
if self.docstatus == 1: if self.docstatus == 1:
if self.status == "Draft": if self.status == "Draft":
@ -162,6 +162,10 @@ class SubcontractingOrder(SubcontractingController):
status = "Completed" status = "Completed"
elif self.per_received > 0 and self.per_received < 100: elif self.per_received > 0 and self.per_received < 100:
status = "Partially Received" status = "Partially Received"
for item in self.supplied_items:
if item.returned_qty:
status = "Closed"
break
else: else:
total_required_qty = total_supplied_qty = 0 total_required_qty = total_supplied_qty = 0
for item in self.supplied_items: for item in self.supplied_items:
@ -176,7 +180,10 @@ class SubcontractingOrder(SubcontractingController):
elif self.docstatus == 2: elif self.docstatus == 2:
status = "Cancelled" status = "Cancelled"
frappe.db.set_value("Subcontracting Order", self.name, "status", status, update_modified) if status:
frappe.db.set_value(
"Subcontracting Order", self.name, "status", status, update_modified=update_modified
)
@frappe.whitelist() @frappe.whitelist()

View File

@ -10,6 +10,7 @@ frappe.listview_settings['Subcontracting Order'] = {
"Completed": "green", "Completed": "green",
"Partial Material Transferred": "purple", "Partial Material Transferred": "purple",
"Material Transferred": "blue", "Material Transferred": "blue",
"Closed": "red",
}; };
return [__(doc.status), status_colors[doc.status], "status,=," + doc.status]; return [__(doc.status), status_colors[doc.status], "status,=," + doc.status];
}, },

View File

@ -7,7 +7,10 @@ import frappe
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_subcontracting_order from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_subcontracting_order
from erpnext.controllers.subcontracting_controller import make_rm_stock_entry from erpnext.controllers.subcontracting_controller import (
get_materials_from_supplier,
make_rm_stock_entry,
)
from erpnext.controllers.tests.test_subcontracting_controller import ( from erpnext.controllers.tests.test_subcontracting_controller import (
get_rm_items, get_rm_items,
get_subcontracting_order, get_subcontracting_order,
@ -89,6 +92,16 @@ class TestSubcontractingOrder(FrappeTestCase):
sco.load_from_db() sco.load_from_db()
self.assertEqual(sco.status, "Partially Received") self.assertEqual(sco.status, "Partially Received")
# Closed
ste = get_materials_from_supplier(sco.name, [d.name for d in sco.supplied_items])
ste.save()
ste.submit()
sco.load_from_db()
self.assertEqual(sco.status, "Closed")
ste.cancel()
sco.load_from_db()
self.assertEqual(sco.status, "Partially Received")
# Completed # Completed
scr = make_subcontracting_receipt(sco.name) scr = make_subcontracting_receipt(sco.name)
scr.save() scr.save()

View File

@ -150,8 +150,7 @@
"label": "Returned Qty", "label": "Returned Qty",
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1, "read_only": 1
"hidden": 1
}, },
{ {
"fieldname": "total_supplied_qty", "fieldname": "total_supplied_qty",
@ -166,7 +165,7 @@
"hide_toolbar": 1, "hide_toolbar": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-04-07 12:58:28.208847", "modified": "2022-08-26 16:04:56.125951",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Subcontracting", "module": "Subcontracting",
"name": "Subcontracting Order Supplied Item", "name": "Subcontracting Order Supplied Item",

View File

@ -369,7 +369,7 @@
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Status", "label": "Status",
"no_copy": 1, "no_copy": 1,
"options": "\nDraft\nCompleted\nReturn\nReturn Issued\nCancelled", "options": "\nDraft\nCompleted\nReturn\nReturn Issued\nCancelled\nClosed",
"print_hide": 1, "print_hide": 1,
"print_width": "150px", "print_width": "150px",
"read_only": 1, "read_only": 1,
@ -628,7 +628,7 @@
"in_create": 1, "in_create": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-08-22 17:30:40.827517", "modified": "2022-08-26 21:02:26.353870",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Subcontracting", "module": "Subcontracting",
"name": "Subcontracting Receipt", "name": "Subcontracting Receipt",

View File

@ -5,14 +5,13 @@ import json
import frappe import frappe
from frappe.utils import cint, cstr from frappe.utils import cint, cstr
from redisearch import AutoCompleter, Client, Query from redis.commands.search.query import Query
from erpnext.e_commerce.redisearch_utils import ( from erpnext.e_commerce.redisearch_utils import (
WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE, WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE,
WEBSITE_ITEM_INDEX, WEBSITE_ITEM_INDEX,
WEBSITE_ITEM_NAME_AUTOCOMPLETE, WEBSITE_ITEM_NAME_AUTOCOMPLETE,
is_redisearch_enabled, is_redisearch_enabled,
make_key,
) )
from erpnext.e_commerce.shopping_cart.product_info import set_product_info_for_website from erpnext.e_commerce.shopping_cart.product_info import set_product_info_for_website
from erpnext.setup.doctype.item_group.item_group import get_item_for_list_in_html from erpnext.setup.doctype.item_group.item_group import get_item_for_list_in_html
@ -88,15 +87,17 @@ def product_search(query, limit=10, fuzzy_search=True):
if not query: if not query:
return search_results return search_results
red = frappe.cache() redis = frappe.cache()
query = clean_up_query(query) query = clean_up_query(query)
# TODO: Check perf/correctness with Suggestions & Query vs only Query # TODO: Check perf/correctness with Suggestions & Query vs only Query
# TODO: Use Levenshtein Distance in Query (max=3) # TODO: Use Levenshtein Distance in Query (max=3)
ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=red) redisearch = redis.ft(WEBSITE_ITEM_INDEX)
client = Client(make_key(WEBSITE_ITEM_INDEX), conn=red) suggestions = redisearch.sugget(
suggestions = ac.get_suggestions( WEBSITE_ITEM_NAME_AUTOCOMPLETE,
query, num=limit, fuzzy=fuzzy_search and len(query) > 3 # Fuzzy on length < 3 can be real slow query,
num=limit,
fuzzy=fuzzy_search and len(query) > 3,
) )
# Build a query # Build a query
@ -106,8 +107,8 @@ def product_search(query, limit=10, fuzzy_search=True):
query_string += f"|('{clean_up_query(s.string)}')" query_string += f"|('{clean_up_query(s.string)}')"
q = Query(query_string) q = Query(query_string)
results = redisearch.search(q)
results = client.search(q)
search_results["results"] = list(map(convert_to_dict, results.docs)) search_results["results"] = list(map(convert_to_dict, results.docs))
search_results["results"] = sorted( search_results["results"] = sorted(
search_results["results"], key=lambda k: frappe.utils.cint(k["ranking"]), reverse=True search_results["results"], key=lambda k: frappe.utils.cint(k["ranking"]), reverse=True
@ -141,8 +142,8 @@ def get_category_suggestions(query):
if not query: if not query:
return search_results return search_results
ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=frappe.cache()) ac = frappe.cache().ft()
suggestions = ac.get_suggestions(query, num=10, with_payloads=True) suggestions = ac.sugget(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE, query, num=10, with_payloads=True)
results = [json.loads(s.payload) for s in suggestions] results = [json.loads(s.payload) for s in suggestions]

View File

@ -12,7 +12,6 @@ dependencies = [
"pycountry~=20.7.3", "pycountry~=20.7.3",
"python-stdnum~=1.16", "python-stdnum~=1.16",
"Unidecode~=1.2.0", "Unidecode~=1.2.0",
"redisearch~=2.1.0",
# integration dependencies # integration dependencies
"gocardless-pro~=1.22.0", "gocardless-pro~=1.22.0",
@ -21,6 +20,9 @@ dependencies = [
"python-youtube~=0.8.0", "python-youtube~=0.8.0",
"taxjar~=1.9.2", "taxjar~=1.9.2",
"tweepy~=3.10.0", "tweepy~=3.10.0",
# Not used directly - required by PyQRCode for PNG generation
"pypng~=0.20220715.0",
] ]
[build-system] [build-system]