Merge branch 'version-13-pre-release' into version-13

This commit is contained in:
Nabin Hait 2021-07-01 12:37:13 +05:30
commit c2b5562a3f
226 changed files with 6357 additions and 2252 deletions

View File

@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides
from frappe.utils import getdate
__version__ = '13.5.2'
__version__ = '13.6.0'
def get_default_company(user=None):
'''Get default company for user'''

View File

@ -33,6 +33,8 @@ def get_shipping_address(company, address = None):
if address and frappe.db.get_value('Dynamic Link',
{'parent': address, 'link_name': company}):
filters.append(["Address", "name", "=", address])
if not address:
filters.append(["Address", "is_shipping_address", "=", 1])
address = frappe.get_all("Address", filters=filters, fields=fields) or {}

View File

@ -19,7 +19,7 @@ class AccountingDimension(Document):
def validate(self):
if self.document_type in core_doctypes_list + ('Accounting Dimension', 'Project',
'Cost Center', 'Accounting Dimension Detail', 'Company') :
'Cost Center', 'Accounting Dimension Detail', 'Company', 'Account') :
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
frappe.throw(msg)

View File

@ -257,9 +257,10 @@
},
{
"default": "1",
"description": "If enabled, ledger entries will be posted for change amount in POS transactions",
"fieldname": "post_change_gl_entries",
"fieldtype": "Check",
"label": "Post Ledger Entries for Given Change"
"label": "Create Ledger Entries for Change Amount"
}
],
"icon": "icon-cog",
@ -267,7 +268,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-05-25 12:34:05.858669",
"modified": "2021-06-17 20:26:03.721202",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@ -54,7 +54,7 @@ class CForm(Document):
frappe.throw(_("Please enter atleast 1 invoice in the table"))
def set_total_invoiced_amount(self):
total = sum([flt(d.grand_total) for d in self.get('invoices')])
total = sum(flt(d.grand_total) for d in self.get('invoices'))
frappe.db.set(self, 'total_invoiced_amount', total)
@frappe.whitelist()

View File

@ -14,7 +14,7 @@ class CouponCode(Document):
if not self.coupon_code:
if self.coupon_type == "Promotional":
self.coupon_code =''.join([i for i in self.coupon_name if not i.isdigit()])[0:8].upper()
self.coupon_code =''.join(i for i in self.coupon_name if not i.isdigit())[0:8].upper()
elif self.coupon_type == "Gift Card":
self.coupon_code = frappe.generate_hash()[:10].upper()

View File

@ -86,7 +86,7 @@ def resolve_dunning(doc, state):
for reference in doc.references:
if reference.reference_doctype == 'Sales Invoice' and reference.outstanding_amount <= 0:
dunnings = frappe.get_list('Dunning', filters={
'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')})
'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')}, ignore_permissions=True)
for dunning in dunnings:
frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved')
@ -96,7 +96,7 @@ def calculate_interest_and_amount(posting_date, outstanding_amount, rate_of_inte
grand_total = 0
if rate_of_interest:
interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100
interest_amount = (interest_per_year * cint(overdue_days)) / 365
interest_amount = (interest_per_year * cint(overdue_days)) / 365
grand_total = flt(outstanding_amount) + flt(interest_amount) + flt(dunning_fee)
dunning_amount = flt(interest_amount) + flt(dunning_fee)
return {

View File

@ -121,8 +121,7 @@ class GLEntry(Document):
def check_pl_account(self):
if self.is_opening=='Yes' and \
frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss" and \
self.voucher_type not in ['Purchase Invoice', 'Sales Invoice']:
frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss":
frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry")
.format(self.voucher_type, self.voucher_no, self.account))

View File

@ -42,7 +42,7 @@ class InvoiceDiscounting(AccountsController):
record.idx, frappe.bold(actual_outstanding), frappe.bold(record.sales_invoice)))
def calculate_total_amount(self):
self.total_amount = sum([flt(d.outstanding_amount) for d in self.invoices])
self.total_amount = sum(flt(d.outstanding_amount) for d in self.invoices)
def on_submit(self):
self.update_sales_invoice()

View File

@ -196,8 +196,8 @@ class JournalEntry(AccountsController):
frappe.throw(_("Row {0}: Party Type and Party is required for Receivable / Payable account {1}").format(d.idx, d.account))
def check_credit_limit(self):
customers = list(set([d.party for d in self.get("accounts")
if d.party_type=="Customer" and d.party and flt(d.debit) > 0]))
customers = list(set(d.party for d in self.get("accounts")
if d.party_type=="Customer" and d.party and flt(d.debit) > 0))
if customers:
from erpnext.selling.doctype.customer.customer import check_credit_limit
for customer in customers:

View File

@ -21,7 +21,7 @@ class MonthlyDistribution(Document):
idx += 1
def validate(self):
total = sum([flt(d.percentage_allocation) for d in self.get("percentages")])
total = sum(flt(d.percentage_allocation) for d in self.get("percentages"))
if flt(total, 2) != 100.0:
frappe.throw(_("Percentage Allocation should be equal to 100%") + \

View File

@ -690,7 +690,7 @@
"options": "Account"
},
{
"depends_on": "eval:doc.received_amount",
"depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'",
"fieldname": "received_amount_after_tax",
"fieldtype": "Currency",
"label": "Received Amount After Tax",
@ -707,7 +707,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-06-09 11:55:04.215050",
"modified": "2021-06-22 20:37:06.154206",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",

View File

@ -310,7 +310,7 @@ class PaymentEntry(AccountsController):
for k, v in no_oustanding_refs.items():
frappe.msgprint(
_("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.")
.format(k, frappe.bold(", ".join([d.reference_name for d in v])), frappe.bold("negative outstanding amount"))
.format(k, frappe.bold(", ".join(d.reference_name for d in v)), frappe.bold("negative outstanding amount"))
+ "<br><br>" + _("If this is undesirable please cancel the corresponding Payment Entry."),
title=_("Warning"), indicator="orange")
@ -511,7 +511,7 @@ class PaymentEntry(AccountsController):
def set_unallocated_amount(self):
self.unallocated_amount = 0
if self.party:
total_deductions = sum([flt(d.amount) for d in self.get("deductions")])
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
if self.payment_type == "Receive" \
and self.base_total_allocated_amount < self.base_received_amount_after_tax + total_deductions \
and self.total_allocated_amount < self.paid_amount_after_tax + (total_deductions / self.source_exchange_rate):
@ -536,7 +536,7 @@ class PaymentEntry(AccountsController):
else:
self.difference_amount = self.base_paid_amount_after_tax - flt(self.base_received_amount_after_tax)
total_deductions = sum([flt(d.amount) for d in self.get("deductions")])
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
self.difference_amount = flt(self.difference_amount - total_deductions,
self.precision("difference_amount"))
@ -552,8 +552,8 @@ class PaymentEntry(AccountsController):
if ((self.payment_type=="Pay" and self.party_type=="Customer")
or (self.payment_type=="Receive" and self.party_type=="Supplier")):
total_negative_outstanding = sum([abs(flt(d.outstanding_amount))
for d in self.get("references") if flt(d.outstanding_amount) < 0])
total_negative_outstanding = sum(abs(flt(d.outstanding_amount))
for d in self.get("references") if flt(d.outstanding_amount) < 0)
paid_amount = self.paid_amount if self.payment_type=="Receive" else self.received_amount
additional_charges = sum([flt(d.amount) for d in self.deductions])
@ -706,7 +706,7 @@ class PaymentEntry(AccountsController):
if account_currency != self.company_currency:
frappe.throw(_("Currency for {0} must be {1}").format(d.account_head, self.company_currency))
if self.payment_type == 'Pay':
if self.payment_type in ('Pay', 'Internal Transfer'):
dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit"
elif self.payment_type == 'Receive':
dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit"
@ -761,7 +761,7 @@ class PaymentEntry(AccountsController):
return self.advance_tax_account
elif self.payment_type == 'Receive':
return self.paid_from
elif self.payment_type == 'Pay':
elif self.payment_type in ('Pay', 'Internal Transfer'):
return self.paid_to
def update_advance_paid(self):

View File

@ -112,7 +112,7 @@ class PaymentRequest(Document):
if not data_of_completed_requests:
return self.grand_total
request_amounts = sum([json.loads(d).get('request_amount') for d in data_of_completed_requests])
request_amounts = sum(json.loads(d).get('request_amount') for d in data_of_completed_requests)
return request_amounts
def on_cancel(self):

View File

@ -26,7 +26,7 @@ class PaymentTermsTemplate(Document):
def check_duplicate_terms(self):
terms = []
for term in self.terms:
term_info = (term.credit_days, term.credit_months, term.due_date_based_on)
term_info = (term.payment_term, term.credit_days, term.credit_months, term.due_date_based_on)
if term_info in terms:
frappe.msgprint(
_('The Payment Term at row {0} is possibly a duplicate.').format(term.idx),

View File

@ -20,9 +20,9 @@ from frappe.utils import cint, flt, get_link_to_form, getdate, today, fmt_money
class MultiplePricingRuleConflict(frappe.ValidationError): pass
apply_on_table = {
'Item Code': 'items',
'Item Group': 'item_groups',
'Brand': 'brands'
'Item Code': 'items',
'Item Group': 'item_groups',
'Brand': 'brands'
}
def get_pricing_rules(args, doc=None):
@ -183,7 +183,7 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
condition = "ifnull({table}.{field}, '') in ({parent_groups})".format(
table=table,
field=field,
parent_groups=", ".join([frappe.db.escape(d) for d in parent_groups])
parent_groups=", ".join(frappe.db.escape(d) for d in parent_groups)
)
frappe.flags.tree_conditions[key] = condition
@ -264,7 +264,7 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
# find pricing rule with highest priority
if pricing_rules:
max_priority = max([cint(p.priority) for p in pricing_rules])
max_priority = max(cint(p.priority) for p in pricing_rules)
if max_priority:
pricing_rules = list(filter(lambda x: cint(x.priority)==max_priority, pricing_rules))
@ -272,14 +272,14 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
pricing_rules = list(pricing_rules)
if len(pricing_rules) > 1:
rate_or_discount = list(set([d.rate_or_discount for d in pricing_rules]))
rate_or_discount = list(set(d.rate_or_discount for d in pricing_rules))
if len(rate_or_discount) == 1 and rate_or_discount[0] == "Discount Percentage":
pricing_rules = list(filter(lambda x: x.for_price_list==args.price_list, pricing_rules)) \
or pricing_rules
if len(pricing_rules) > 1 and not args.for_shopping_cart:
frappe.throw(_("Multiple Price Rules exists with same criteria, please resolve conflict by assigning priority. Price Rules: {0}")
.format("\n".join([d.name for d in pricing_rules])), MultiplePricingRuleConflict)
.format("\n".join(d.name for d in pricing_rules)), MultiplePricingRuleConflict)
elif pricing_rules:
return pricing_rules[0]
@ -541,7 +541,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values=False):
if pricing_rule_args:
items = tuple([(d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item])
items = tuple((d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item)
for args in pricing_rule_args:
if not items or (args.get('item_code'), args.get('pricing_rules')) not in items:
@ -589,4 +589,4 @@ def update_coupon_code_count(coupon_name,transaction_type):
elif transaction_type=='cancelled':
if coupon.used>0:
coupon.used=coupon.used-1
coupon.save(ignore_permissions=True)
coupon.save(ignore_permissions=True)

View File

@ -27,10 +27,6 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
});
},
company: function() {
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
},
onload: function() {
this._super();
@ -569,5 +565,9 @@ frappe.ui.form.on("Purchase Invoice", {
frm: frm,
freeze_message: __("Creating Purchase Receipt ...")
})
}
},
company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
})

View File

@ -400,6 +400,7 @@ class PurchaseInvoice(BuyingController):
# because updating ordered qty in bin depends upon updated ordered qty in PO
if self.update_stock == 1:
self.update_stock_ledger()
self.set_consumed_qty_in_po()
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
update_serial_nos_after_submit(self, "items")
@ -998,6 +999,7 @@ class PurchaseInvoice(BuyingController):
if self.update_stock == 1:
self.update_stock_ledger()
self.delete_auto_created_batches()
self.set_consumed_qty_in_po()
self.make_gl_entries_on_cancel()

View File

@ -621,8 +621,10 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(actual_qty_0, get_qty_after_transaction())
def test_subcontracting_via_purchase_invoice(self):
from erpnext.buying.doctype.purchase_order.test_purchase_order import update_backflush_based_on
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
update_backflush_based_on('BOM')
make_stock_entry(item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100)
make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse 1 - _TC",
qty=100, basic_rate=100)
@ -632,7 +634,7 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(len(pi.get("supplied_items")), 2)
rm_supp_cost = sum([d.amount for d in pi.get("supplied_items")])
rm_supp_cost = sum(d.amount for d in pi.get("supplied_items"))
self.assertEqual(flt(pi.get("items")[0].rm_supp_cost, 2), flt(rm_supp_cost, 2))
def test_rejected_serial_no(self):
@ -964,7 +966,7 @@ class TestPurchaseInvoice(unittest.TestCase):
update_tax_witholding_category('_Test Company', 'TDS Payable - _TC', nowdate())
# Create Purchase Order with TDS applied
po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000)
po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item')
po.apply_tds = 1
po.tax_withholding_category = 'TDS - 194 - Dividends - Individual'
po.save()
@ -1000,6 +1002,7 @@ class TestPurchaseInvoice(unittest.TestCase):
# Create Purchase Invoice against Purchase Order
purchase_invoice = get_mapped_purchase_invoice(po.name)
purchase_invoice.allocate_advances_automatically = 1
purchase_invoice.items[0].item_code = '_Test Non Stock Item'
purchase_invoice.items[0].expense_account = '_Test Account Cost for Goods Sold - _TC'
purchase_invoice.save()
purchase_invoice.submit()

View File

@ -272,7 +272,7 @@
"fieldname": "rate",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Rate ",
"label": "Rate",
"oldfieldname": "import_rate",
"oldfieldtype": "Currency",
"options": "currency",
@ -854,7 +854,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-03-30 09:02:39.256602",
"modified": "2021-06-16 19:57:03.101571",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@ -112,7 +112,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
si = create_sales_invoice(customer = "Test TCS Customer", rate=5000)
si.submit()
tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC'])
tcs_charged = sum(d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC')
self.assertEqual(tcs_charged, 500)
invoices.append(si)

View File

@ -101,7 +101,7 @@ def merge_similar_entries(gl_map, precision=None):
def check_if_in_list(gle, gl_map, dimensions=None):
account_head_fieldnames = ['party_type', 'party', 'against_voucher', 'against_voucher_type',
'cost_center', 'project']
'cost_center', 'project', 'voucher_detail_no']
if dimensions:
account_head_fieldnames = account_head_fieldnames + dimensions
@ -143,7 +143,7 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
validate_expense_against_budget(args)
def validate_cwip_accounts(gl_map):
cwip_enabled = any([cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")])
cwip_enabled = any(cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting"))
if cwip_enabled and gl_map[0].voucher_type == "Journal Entry":
cwip_accounts = [d[0] for d in frappe.db.sql("""select name from tabAccount

View File

@ -32,7 +32,7 @@ def get_accounts_in_mappers(mapping_names):
join `tabCash Flow Mapping` cfm on cfma.parent=cfm.name
where cfma.parent in (%s)
order by cfm.is_working_capital
''', (', '.join(['"%s"' % d for d in mapping_names])))
''', (', '.join('"%s"' % d for d in mapping_names)))
def setup_mappers(mappers):
@ -83,8 +83,8 @@ def setup_mappers(mappers):
account_types_labels = sorted(
set(
[(d['label'], d['is_working_capital'], d['is_income_tax_liability'], d['is_income_tax_expense'])
for d in account_types]
(d['label'], d['is_working_capital'], d['is_income_tax_liability'], d['is_income_tax_expense'])
for d in account_types
),
key=lambda x: x[1]
)
@ -375,7 +375,7 @@ def _get_account_type_based_data(filters, account_names, period_list, accumulate
total = 0
for period in period_list:
start_date = get_start_date(period, accumulated_values, company)
accounts = ', '.join(['"%s"' % d for d in account_names])
accounts = ', '.join('"%s"' % d for d in account_names)
if opening_balances:
date_info = dict(date=start_date)

View File

@ -145,7 +145,7 @@ class PartyLedgerSummaryReport(object):
out = []
for party, row in iteritems(self.party_data):
if row.opening_balance or row.invoiced_amount or row.paid_amount or row.return_amount or row.closing_amount:
total_party_adjustment = sum([amount for amount in itervalues(self.party_adjustment_details.get(party, {}))])
total_party_adjustment = sum(amount for amount in itervalues(self.party_adjustment_details.get(party, {})))
row.paid_amount -= total_party_adjustment
adjustments = self.party_adjustment_details.get(party, {})

View File

@ -369,7 +369,7 @@ def set_gl_entries_by_account(
if accounts:
additional_conditions += " and account in ({})"\
.format(", ".join([frappe.db.escape(d) for d in accounts]))
.format(", ".join(frappe.db.escape(d) for d in accounts))
gl_filters = {
"company": company,

View File

@ -36,16 +36,12 @@ frappe.query_reports["General Ledger"] = {
{
"fieldname":"account",
"label": __("Account"),
"fieldtype": "Link",
"fieldtype": "MultiSelectList",
"options": "Account",
"get_query": function() {
var company = frappe.query_report.get_filter_value('company');
return {
"doctype": "Account",
"filters": {
"company": company,
}
}
get_data: function(txt) {
return frappe.db.get_link_options('Account', txt, {
company: frappe.query_report.get_filter_value("company")
});
}
},
{
@ -135,7 +131,9 @@ frappe.query_reports["General Ledger"] = {
"label": __("Cost Center"),
"fieldtype": "MultiSelectList",
get_data: function(txt) {
return frappe.db.get_link_options('Cost Center', txt);
return frappe.db.get_link_options('Cost Center', txt, {
company: frappe.query_report.get_filter_value("company")
});
}
},
{
@ -143,7 +141,9 @@ frappe.query_reports["General Ledger"] = {
"label": __("Project"),
"fieldtype": "MultiSelectList",
get_data: function(txt) {
return frappe.db.get_link_options('Project', txt);
return frappe.db.get_link_options('Project', txt, {
company: frappe.query_report.get_filter_value("company")
});
}
},
{

View File

@ -49,8 +49,12 @@ def validate_filters(filters, account_details):
if not filters.get("from_date") and not filters.get("to_date"):
frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date"))))
if filters.get("account") and not account_details.get(filters.account):
frappe.throw(_("Account {0} does not exists").format(filters.account))
for account in filters.account:
if not account_details.get(account):
frappe.throw(_("Account {0} does not exists").format(account))
if filters.get('account'):
filters.account = frappe.parse_json(filters.get('account'))
if (filters.get("account") and filters.get("group_by") == _('Group by Account')
and account_details[filters.account].is_group == 0):
@ -87,7 +91,19 @@ def set_account_currency(filters):
account_currency = None
if filters.get("account"):
account_currency = get_account_currency(filters.account)
if len(filters.get("account")) == 1:
account_currency = get_account_currency(filters.account[0])
else:
currency = get_account_currency(filters.account[0])
is_same_account_currency = True
for account in filters.get("account"):
if get_account_currency(account) != currency:
is_same_account_currency = False
break
if is_same_account_currency:
account_currency = currency
elif filters.get("party"):
gle_currency = frappe.db.get_value(
"GL Entry", {
@ -205,10 +221,10 @@ def get_gl_entries(filters, accounting_dimensions):
def get_conditions(filters):
conditions = []
if filters.get("account") and not filters.get("include_dimensions"):
lft, rgt = frappe.db.get_value("Account", filters["account"], ["lft", "rgt"])
conditions.append("""account in (select name from tabAccount
where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt))
if filters.get("account"):
filters.account = get_accounts_with_children(filters.account)
conditions.append("account in %(account)s")
if filters.get("cost_center"):
filters.cost_center = get_cost_centers_with_children(filters.cost_center)
@ -266,6 +282,20 @@ def get_conditions(filters):
return "and {}".format(" and ".join(conditions)) if conditions else ""
def get_accounts_with_children(accounts):
if not isinstance(accounts, list):
accounts = [d.strip() for d in accounts.strip().split(',') if d]
all_accounts = []
for d in accounts:
if frappe.db.exists("Account", d):
lft, rgt = frappe.db.get_value("Account", d, ["lft", "rgt"])
children = frappe.get_all("Account", filters={"lft": [">=", lft], "rgt": ["<=", rgt]})
all_accounts += [c.name for c in children]
else:
frappe.throw(_("Account: {0} does not exist").format(d))
return list(set(all_accounts))
def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries):
data = []

View File

@ -334,7 +334,7 @@ def get_aii_accounts():
def get_purchase_receipts_against_purchase_order(item_list):
po_pr_map = frappe._dict()
po_item_rows = list(set([d.po_detail for d in item_list]))
po_item_rows = list(set(d.po_detail for d in item_list))
if po_item_rows:
purchase_receipts = frappe.db.sql("""

View File

@ -23,7 +23,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
if item_list:
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
mode_of_payments = get_mode_of_payments(set([d.parent for d in item_list]))
mode_of_payments = get_mode_of_payments(set(d.parent for d in item_list))
so_dn_map = get_delivery_notes_against_sales_order(item_list)
data = []

View File

@ -77,14 +77,14 @@ def get_pos_entries(filters, group_by_field):
), filters, as_dict=1)
def concat_mode_of_payments(pos_entries):
mode_of_payments = get_mode_of_payments(set([d.pos_invoice for d in pos_entries]))
mode_of_payments = get_mode_of_payments(set(d.pos_invoice for d in pos_entries))
for entry in pos_entries:
if mode_of_payments.get(entry.pos_invoice):
entry.mode_of_payment = ", ".join(mode_of_payments.get(entry.pos_invoice, []))
def add_subtotal_row(data, group_invoices, group_by_field, group_by_value):
grand_total = sum([d.grand_total for d in group_invoices])
paid_amount = sum([d.paid_amount for d in group_invoices])
grand_total = sum(d.grand_total for d in group_invoices)
paid_amount = sum(d.paid_amount for d in group_invoices)
data.append({
group_by_field: group_by_value,
"grand_total": grand_total,

View File

@ -26,7 +26,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
invoice_expense_map, invoice_tax_map = get_invoice_tax_map(invoice_list,
invoice_expense_map, expense_accounts)
invoice_po_pr_map = get_invoice_po_pr_map(invoice_list)
suppliers = list(set([d.supplier for d in invoice_list]))
suppliers = list(set(d.supplier for d in invoice_list))
supplier_details = get_supplier_details(suppliers)
company_currency = frappe.get_cached_value('Company', filters.company, "default_currency")
@ -120,13 +120,13 @@ def get_columns(invoice_list, additional_table_columns):
and docstatus = 1 and (account_head is not null and account_head != '')
and category in ('Total', 'Valuation and Total')
and parent in (%s) order by account_head""" %
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list))
unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account
from `tabPurchase Invoice` where docstatus = 1 and name in (%s)
and ifnull(unrealized_profit_loss_account, '') != ''
order by unrealized_profit_loss_account""" %
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list))
expense_columns = [(account + ":Currency/currency:120") for account in expense_accounts]
unrealized_profit_loss_account_columns = [(account + ":Currency/currency:120") for account in unrealized_profit_loss_accounts]
@ -208,7 +208,7 @@ def get_invoice_expense_map(invoice_list):
from `tabPurchase Invoice Item`
where parent in (%s)
group by parent, expense_account
""" % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
""" % ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
invoice_expense_map = {}
for d in expense_details:
@ -221,7 +221,7 @@ def get_internal_invoice_map(invoice_list):
unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account,
base_net_total as amount from `tabPurchase Invoice` where name in (%s)
and is_internal_supplier = 1 and company = represents_company""" %
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
internal_invoice_map = {}
for d in unrealized_amount_details:
@ -238,7 +238,7 @@ def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts):
where parent in (%s) and category in ('Total', 'Valuation and Total')
and base_tax_amount_after_discount_amount != 0
group by parent, account_head, add_deduct_tax
""" % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
""" % ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
invoice_tax_map = {}
for d in tax_details:
@ -258,7 +258,7 @@ def get_invoice_po_pr_map(invoice_list):
select parent, purchase_order, purchase_receipt, po_detail, project
from `tabPurchase Invoice Item`
where parent in (%s)
""" % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
""" % ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
invoice_po_pr_map = {}
for d in pi_items:

View File

@ -158,7 +158,7 @@ def get_sales_invoice_data(filters):
def get_mode_of_payments(filters):
mode_of_payments = {}
invoice_list = get_invoices(filters)
invoice_list_names = ",".join(['"' + invoice['name'] + '"' for invoice in invoice_list])
invoice_list_names = ",".join('"' + invoice['name'] + '"' for invoice in invoice_list)
if invoice_list:
inv_mop = frappe.db.sql("""select a.owner,a.posting_date, ifnull(b.mode_of_payment, '') as mode_of_payment
from `tabSales Invoice` a, `tabSales Invoice Payment` b
@ -197,7 +197,7 @@ def get_invoices(filters):
def get_mode_of_payment_details(filters):
mode_of_payment_details = {}
invoice_list = get_invoices(filters)
invoice_list_names = ",".join(['"' + invoice['name'] + '"' for invoice in invoice_list])
invoice_list_names = ",".join('"' + invoice['name'] + '"' for invoice in invoice_list)
if invoice_list:
inv_mop_detail = frappe.db.sql("""select a.owner, a.posting_date,
ifnull(b.mode_of_payment, '') as mode_of_payment, sum(b.base_amount) as paid_amount

View File

@ -248,19 +248,19 @@ def get_columns(invoice_list, additional_table_columns):
income_accounts = frappe.db.sql_list("""select distinct income_account
from `tabSales Invoice Item` where docstatus = 1 and parent in (%s)
order by income_account""" %
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list))
tax_accounts = frappe.db.sql_list("""select distinct account_head
from `tabSales Taxes and Charges` where parenttype = 'Sales Invoice'
and docstatus = 1 and base_tax_amount_after_discount_amount != 0
and parent in (%s) order by account_head""" %
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list))
unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account
from `tabSales Invoice` where docstatus = 1 and name in (%s)
and ifnull(unrealized_profit_loss_account, '') != ''
order by unrealized_profit_loss_account""" %
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list))
for account in income_accounts:
income_columns.append({
@ -406,7 +406,7 @@ def get_invoices(filters, additional_query_columns):
def get_invoice_income_map(invoice_list):
income_details = frappe.db.sql("""select parent, income_account, sum(base_net_amount) as amount
from `tabSales Invoice Item` where parent in (%s) group by parent, income_account""" %
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
invoice_income_map = {}
for d in income_details:
@ -419,7 +419,7 @@ def get_internal_invoice_map(invoice_list):
unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account,
base_net_total as amount from `tabSales Invoice` where name in (%s)
and is_internal_customer = 1 and company = represents_company""" %
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
internal_invoice_map = {}
for d in unrealized_amount_details:
@ -432,7 +432,7 @@ def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts):
tax_details = frappe.db.sql("""select parent, account_head,
sum(base_tax_amount_after_discount_amount) as tax_amount
from `tabSales Taxes and Charges` where parent in (%s) group by parent, account_head""" %
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
invoice_tax_map = {}
for d in tax_details:
@ -451,7 +451,7 @@ def get_invoice_so_dn_map(invoice_list):
si_items = frappe.db.sql("""select parent, sales_order, delivery_note, so_detail
from `tabSales Invoice Item` where parent in (%s)
and (ifnull(sales_order, '') != '' or ifnull(delivery_note, '') != '')""" %
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
invoice_so_dn_map = {}
for d in si_items:
@ -475,7 +475,7 @@ def get_invoice_cc_wh_map(invoice_list):
si_items = frappe.db.sql("""select parent, cost_center, warehouse
from `tabSales Invoice Item` where parent in (%s)
and (ifnull(cost_center, '') != '' or ifnull(warehouse, '') != '')""" %
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
invoice_cc_wh_map = {}
for d in si_items:

View File

@ -78,7 +78,7 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, f
and company=%s and posting_date between %s and %s
""", (supplier, company, from_date, to_date), as_dict=1)
supplier_credit_amount = flt(sum([d.credit for d in entries]))
supplier_credit_amount = flt(sum(d.credit for d in entries))
vouchers = [d.voucher_no for d in entries]
vouchers += get_advance_vouchers([supplier], company=company,
@ -91,7 +91,7 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, f
from `tabGL Entry`
where account=%s and posting_date between %s and %s
and company=%s and credit > 0 and voucher_no in ({0})
""".format(', '.join(["'%s'" % d for d in vouchers])),
""".format(', '.join("'%s'" % d for d in vouchers)),
(account, from_date, to_date, company))[0][0])
date_range_filter = [fiscal_year, from_date, to_date]

View File

@ -139,6 +139,6 @@ def get_invoiced_item_gross_margin(sales_invoice=None, item_code=None, company=N
gross_profit_data = GrossProfitGenerator(filters)
result = gross_profit_data.grouped_data
if not with_item_data:
result = sum([d.gross_profit for d in result])
result = sum(d.gross_profit for d in result)
return result

View File

@ -635,7 +635,7 @@ def get_held_invoices(party_type, party):
'select name from `tabPurchase Invoice` where release_date IS NOT NULL and release_date > CURDATE()',
as_dict=1
)
held_invoices = set([d['name'] for d in held_invoices])
held_invoices = set(d['name'] for d in held_invoices)
return held_invoices

View File

@ -470,7 +470,7 @@ class TestAsset(unittest.TestCase):
})
asset.insert()
accumulated_depreciation_after_full_schedule = \
max([d.accumulated_depreciation_amount for d in asset.get("schedules")])
max(d.accumulated_depreciation_amount for d in asset.get("schedules"))
asset_value_after_full_schedule = (flt(asset.gross_purchase_amount) -
flt(accumulated_depreciation_after_full_schedule))

View File

@ -92,7 +92,7 @@ class AssetValueAdjustment(Document):
d.value_after_depreciation = asset_value
if d.depreciation_method in ("Straight Line", "Manual"):
end_date = max([s.schedule_date for s in asset.schedules if cint(s.finance_book_id) == d.idx])
end_date = max(s.schedule_date for s in asset.schedules if cint(s.finance_book_id) == d.idx)
total_days = date_diff(end_date, self.date)
rate_per_day = flt(d.value_after_depreciation) / flt(total_days)
from_date = self.date

View File

@ -9,13 +9,14 @@
"supp_master_name",
"supplier_group",
"buying_price_list",
"maintain_same_rate_action",
"role_to_override_stop_action",
"column_break_3",
"po_required",
"pr_required",
"maintain_same_rate",
"maintain_same_rate_action",
"role_to_override_stop_action",
"allow_multiple_items",
"bill_for_rejected_quantity_in_purchase_invoice",
"subcontract",
"backflush_raw_materials_of_subcontract_based_on",
"column_break_11",
@ -108,6 +109,13 @@
"fieldtype": "Link",
"label": "Role Allowed to Override Stop Action",
"options": "Role"
},
{
"default": "1",
"description": "If checked, Rejected Quantity will be included while making Purchase Invoice from Purchase Receipt.",
"fieldname": "bill_for_rejected_quantity_in_purchase_invoice",
"fieldtype": "Check",
"label": "Bill for Rejected Quantity in Purchase Invoice"
}
],
"icon": "fa fa-cog",
@ -115,7 +123,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-04-04 20:01:44.087066",
"modified": "2021-06-24 10:38:28.934525",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",

View File

@ -53,6 +53,39 @@ frappe.ui.form.on("Purchase Order", {
} else {
frm.set_value("tax_withholding_category", frm.supplier_tds);
}
},
refresh: function(frm) {
frm.trigger('get_materials_from_supplier');
},
get_materials_from_supplier: function(frm) {
let po_details = [];
if (frm.doc.supplied_items && (frm.doc.per_received == 100 || frm.doc.status === 'Closed')) {
frm.doc.supplied_items.forEach(d => {
if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) {
po_details.push(d.name)
}
});
}
if (po_details && po_details.length) {
frm.add_custom_button(__('Return of Components'), () => {
frm.call({
method: 'erpnext.buying.doctype.purchase_order.purchase_order.get_materials_from_supplier',
freeze: true,
freeze_message: __('Creating Stock Entry'),
args: { purchase_order: frm.doc.name, po_details: po_details },
callback: function(r) {
if (r && r.message) {
const doc = frappe.model.sync(r.message);
frappe.set_route("Form", doc[0].doctype, doc[0].name);
}
}
});
}, __('Create'));
}
}
});
@ -217,7 +250,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
},
has_unsupplied_items: function() {
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)
},
make_stock_entry: function() {
@ -513,12 +546,14 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
],
primary_action: function() {
var data = d.get_values();
let reason_for_hold = 'Reason for hold: ' + data.reason_for_hold;
frappe.call({
method: "frappe.desk.form.utils.add_comment",
args: {
reference_doctype: me.frm.doctype,
reference_name: me.frm.docname,
content: __('Reason for hold:') + " " +data.reason_for_hold,
content: __(reason_for_hold),
comment_email: frappe.session.user,
comment_by: frappe.session.user_fullname
},

View File

@ -609,6 +609,7 @@
"fieldname": "supplied_items",
"fieldtype": "Table",
"label": "Supplied Items",
"no_copy": 1,
"oldfieldname": "po_raw_material_details",
"oldfieldtype": "Table",
"options": "Purchase Order Item Supplied",
@ -1377,7 +1378,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2021-04-19 00:55:30.781375",
"modified": "2021-05-30 15:17:53.663648",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",

View File

@ -14,12 +14,11 @@ from frappe.desk.notifications import clear_doctype_notifications
from erpnext.buying.utils import validate_for_items, check_on_hold_or_closed_status
from erpnext.stock.utils import get_bin
from erpnext.accounts.party import get_party_account_currency
from six import string_types
from erpnext.stock.doctype.item.item import get_item_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
from erpnext.accounts.doctype.sales_invoice.sales_invoice import validate_inter_company_party, update_linked_doc,\
unlink_inter_company_doc
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (validate_inter_company_party,
update_linked_doc, unlink_inter_company_doc)
form_grid_templates = {
"items": "templates/form_grid/item_grid.html"
@ -139,7 +138,7 @@ class PurchaseOrder(BuyingController):
def validate_minimum_order_qty(self):
if not self.get("items"): return
items = list(set([d.item_code for d in self.get("items")]))
items = list(set(d.item_code for d in self.get("items")))
itemwise_min_order_qty = frappe._dict(frappe.db.sql("""select name, min_order_qty
from tabItem where name in ({0})""".format(", ".join(["%s"] * len(items))), items))
@ -326,10 +325,10 @@ class PurchaseOrder(BuyingController):
so.notify_update()
def has_drop_ship_item(self):
return any([d.delivered_by_supplier for d in self.items])
return any(d.delivered_by_supplier for d in self.items)
def is_against_so(self):
return any([d.sales_order for d in self.items if d.sales_order])
return any(d.sales_order for d in self.items if d.sales_order)
def set_received_qty_for_drop_ship_items(self):
for item in self.items:
@ -503,9 +502,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
@frappe.whitelist()
def make_rm_stock_entry(purchase_order, rm_items):
if isinstance(rm_items, string_types):
rm_items_list = rm_items
if isinstance(rm_items, str):
rm_items_list = json.loads(rm_items)
else:
elif not rm_items:
frappe.throw(_("No Items available for transfer"))
if rm_items_list:
@ -543,6 +544,8 @@ def make_rm_stock_entry(purchase_order, rm_items):
'qty': rm_item_data["qty"],
'from_warehouse': rm_item_data["warehouse"],
'stock_uom': rm_item_data["stock_uom"],
'serial_no': rm_item_data.get('serial_no'),
'batch_no': rm_item_data.get('batch_no'),
'main_item_code': rm_item_data["item_code"],
'allow_alternative_item': item_wh.get(rm_item_code, {}).get('allow_alternative_item')
}
@ -582,3 +585,58 @@ def update_status(status, name):
def make_inter_company_sales_order(source_name, target_doc=None):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction
return make_inter_company_transaction("Purchase Order", source_name, target_doc)
@frappe.whitelist()
def get_materials_from_supplier(purchase_order, po_details):
if isinstance(po_details, str):
po_details = json.loads(po_details)
doc = frappe.get_cached_doc('Purchase Order', purchase_order)
doc.initialized_fields()
doc.purchase_orders = [doc.name]
doc.get_available_materials()
if not doc.available_materials:
frappe.throw(_('Materials are already received against the purchase order {0}')
.format(purchase_order))
return make_return_stock_entry_for_subcontract(doc.available_materials, doc, po_details)
def make_return_stock_entry_for_subcontract(available_materials, po_doc, po_details):
ste_doc = frappe.new_doc('Stock Entry')
ste_doc.purpose = 'Material Transfer'
ste_doc.purchase_order = po_doc.name
ste_doc.company = po_doc.company
ste_doc.is_return = 1
for key, value in available_materials.items():
if not value.qty:
continue
if value.batch_no:
for batch_no, qty in value.batch_no.items():
if qty > 0:
add_items_in_ste(ste_doc, value, value.qty, po_details, batch_no)
else:
add_items_in_ste(ste_doc, value, value.qty, po_details)
ste_doc.set_stock_entry_type()
ste_doc.calculate_rate_and_amount()
return ste_doc
def add_items_in_ste(ste_doc, row, qty, po_details, batch_no=None):
item = ste_doc.append('items', row.item_details)
po_detail = list(set(row.po_details).intersection(po_details))
item.update({
'qty': qty,
'batch_no': batch_no,
'basic_rate': row.item_details['rate'],
'po_detail': po_detail[0] if po_detail else '',
's_warehouse': row.item_details['t_warehouse'],
't_warehouse': row.item_details['s_warehouse'],
'item_code': row.item_details['rm_item_code'],
'subcontracted_item': row.item_details['main_item_code'],
'serial_no': '\n'.join(row.serial_no) if row.serial_no else ''
})

View File

@ -20,7 +20,6 @@ from erpnext.controllers.status_updater import OverAllowanceError
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order
from erpnext.stock.doctype.batch.test_batch import make_new_batch
from erpnext.controllers.buying_controller import get_backflushed_subcontracted_raw_materials
class TestPurchaseOrder(unittest.TestCase):
def test_make_purchase_receipt(self):
@ -359,7 +358,7 @@ class TestPurchaseOrder(unittest.TestCase):
update_child_qty_rate('Purchase Order', trans_item, po.name)
po.reload()
total_reqd_qty_after_change = sum([d.get("required_qty") for d in po.as_dict().get("supplied_items")])
total_reqd_qty_after_change = sum(d.get("required_qty") for d in po.as_dict().get("supplied_items"))
self.assertEqual(total_reqd_qty_after_change, 2 * total_reqd_qty)
@ -771,7 +770,7 @@ class TestPurchaseOrder(unittest.TestCase):
self.assertEqual(bin11.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
def test_exploded_items_in_subcontracted(self):
item_code = "_Test Subcontracted FG Item 1"
item_code = "_Test Subcontracted FG Item 11"
make_subcontracted_item(item_code=item_code)
po = create_purchase_order(item_code=item_code, qty=1,
@ -848,79 +847,6 @@ class TestPurchaseOrder(unittest.TestCase):
for item in rm_items:
transferred_rm_map[item.get('rm_item_code')] = item
for item in pr.get('supplied_items'):
self.assertEqual(item.get('required_qty'), (transferred_rm_map[item.get('rm_item_code')].get('qty') / order_qty) * received_qty)
update_backflush_based_on("BOM")
def test_backflushed_based_on_for_multiple_batches(self):
item_code = "_Test Subcontracted FG Item 2"
make_item('Sub Contracted Raw Material 2', {
'is_stock_item': 1,
'is_sub_contracted_item': 1
})
make_subcontracted_item(item_code=item_code, has_batch_no=1, create_new_batch=1,
raw_materials=["Sub Contracted Raw Material 2"])
update_backflush_based_on("Material Transferred for Subcontract")
order_qty = 500
po = create_purchase_order(item_code=item_code, qty=order_qty,
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
make_stock_entry(target="_Test Warehouse - _TC",
item_code = "Sub Contracted Raw Material 2", qty=552, basic_rate=100)
rm_items = [
{"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 2","item_name":"_Test Item",
"qty":552,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}]
rm_item_string = json.dumps(rm_items)
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
se.submit()
for batch in ["ABCD1", "ABCD2", "ABCD3", "ABCD4"]:
make_new_batch(batch_id=batch, item_code=item_code)
pr = make_purchase_receipt(po.name)
# partial receipt
pr.get('items')[0].qty = 30
pr.get('items')[0].batch_no = "ABCD1"
purchase_order = po.name
purchase_order_item = po.items[0].name
for batch_no, qty in {"ABCD2": 60, "ABCD3": 70, "ABCD4":40}.items():
pr.append("items", {
"item_code": pr.get('items')[0].item_code,
"item_name": pr.get('items')[0].item_name,
"uom": pr.get('items')[0].uom,
"stock_uom": pr.get('items')[0].stock_uom,
"warehouse": pr.get('items')[0].warehouse,
"conversion_factor": pr.get('items')[0].conversion_factor,
"cost_center": pr.get('items')[0].cost_center,
"rate": pr.get('items')[0].rate,
"qty": qty,
"batch_no": batch_no,
"purchase_order": purchase_order,
"purchase_order_item": purchase_order_item
})
pr.submit()
pr1 = make_purchase_receipt(po.name)
pr1.get('items')[0].qty = 300
pr1.get('items')[0].batch_no = "ABCD1"
pr1.save()
pr_key = ("Sub Contracted Raw Material 2", po.name)
consumed_qty = get_backflushed_subcontracted_raw_materials([po.name]).get(pr_key)
self.assertTrue(pr1.supplied_items[0].consumed_qty > 0)
self.assertTrue(pr1.supplied_items[0].consumed_qty, flt(552.0) - flt(consumed_qty))
update_backflush_based_on("BOM")
def test_supplied_qty_against_subcontracted_po(self):
@ -1117,22 +1043,29 @@ def create_purchase_order(**args):
po.conversion_factor = args.conversion_factor or 1
po.supplier_warehouse = args.supplier_warehouse or None
po.append("items", {
"item_code": args.item or args.item_code or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": args.qty or 10,
"rate": args.rate or 500,
"schedule_date": add_days(nowdate(), 1),
"include_exploded_items": args.get('include_exploded_items', 1),
"against_blanket_order": args.against_blanket_order
})
if args.rm_items:
for row in args.rm_items:
po.append("items", row)
else:
po.append("items", {
"item_code": args.item or args.item_code or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": args.qty or 10,
"rate": args.rate or 500,
"schedule_date": add_days(nowdate(), 1),
"include_exploded_items": args.get('include_exploded_items', 1),
"against_blanket_order": args.against_blanket_order
})
po.set_missing_values()
if not args.do_not_save:
po.insert()
if not args.do_not_submit:
if po.is_subcontracted == "Yes":
supp_items = po.get("supplied_items")
for d in supp_items:
d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC"
if not d.reserve_warehouse:
d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC"
po.submit()
return po

View File

@ -6,21 +6,25 @@
"engine": "InnoDB",
"field_order": [
"main_item_code",
"bom_detail_no",
"rm_item_code",
"column_break_3",
"stock_uom",
"reserve_warehouse",
"conversion_factor",
"column_break_6",
"rm_item_code",
"bom_detail_no",
"reference_name",
"reserve_warehouse",
"section_break2",
"rate",
"col_break2",
"amount",
"section_break1",
"required_qty",
"supplied_qty",
"col_break1",
"supplied_qty"
"consumed_qty",
"returned_qty",
"total_supplied_qty"
],
"fields": [
{
@ -125,6 +129,8 @@
"fieldtype": "Float",
"in_list_view": 1,
"label": "Supplied Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
@ -142,13 +148,42 @@
{
"fieldname": "col_break2",
"fieldtype": "Column Break"
},
{
"fieldname": "consumed_qty",
"fieldtype": "Float",
"label": "Consumed Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "returned_qty",
"fieldtype": "Float",
"label": "Returned Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "total_supplied_qty",
"fieldtype": "Float",
"hidden": 1,
"label": "Total Supplied Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
}
],
"hide_toolbar": 1,
"idx": 1,
"istable": 1,
"links": [],
"modified": "2020-09-18 17:26:09.703215",
"modified": "2021-06-09 15:17:58.128242",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item Supplied",

View File

@ -6,10 +6,11 @@
"engine": "InnoDB",
"field_order": [
"main_item_code",
"description",
"rm_item_code",
"item_name",
"bom_detail_no",
"col_break1",
"rm_item_code",
"description",
"stock_uom",
"conversion_factor",
"reference_name",
@ -25,7 +26,8 @@
"secbreak_3",
"batch_no",
"col_break4",
"serial_no"
"serial_no",
"purchase_order"
],
"fields": [
{
@ -52,7 +54,6 @@
"fieldname": "description",
"fieldtype": "Text Editor",
"in_global_search": 1,
"in_list_view": 1,
"label": "Description",
"oldfieldname": "description",
"oldfieldtype": "Data",
@ -81,18 +82,20 @@
"fieldname": "required_qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Required Qty",
"label": "Available Qty For Consumption",
"oldfieldname": "required_qty",
"oldfieldtype": "Currency",
"print_hide": 1,
"read_only": 1
},
{
"columns": 2,
"fieldname": "consumed_qty",
"fieldtype": "Float",
"label": "Consumed Qty",
"in_list_view": 1,
"label": "Qty to Be Consumed",
"oldfieldname": "consumed_qty",
"oldfieldtype": "Currency",
"read_only": 1,
"reqd": 1
},
{
@ -183,12 +186,28 @@
{
"fieldname": "col_break4",
"fieldtype": "Column Break"
},
{
"fieldname": "item_name",
"fieldtype": "Data",
"label": "Item Name",
"read_only": 1
},
{
"fieldname": "purchase_order",
"fieldtype": "Link",
"hidden": 1,
"label": "Purchase Order",
"no_copy": 1,
"options": "Purchase Order",
"print_hide": 1,
"read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2020-09-18 17:26:09.703215",
"modified": "2021-06-19 19:33:04.431213",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Receipt Item Supplied",

View File

@ -317,19 +317,21 @@ def add_items(sq_doc, supplier, items):
create_rfq_items(sq_doc, supplier, data)
def create_rfq_items(sq_doc, supplier, data):
sq_doc.append('items', {
"item_code": data.item_code,
"item_name": data.item_name,
"description": data.description,
"qty": data.qty,
"rate": data.rate,
"conversion_factor": data.conversion_factor if data.conversion_factor else None,
"supplier_part_no": frappe.db.get_value("Item Supplier", {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no"),
"warehouse": data.warehouse or '',
args = {}
for field in ['item_code', 'item_name', 'description', 'qty', 'rate', 'conversion_factor',
'warehouse', 'material_request', 'material_request_item', 'stock_qty']:
args[field] = data.get(field)
args.update({
"request_for_quotation_item": data.name,
"request_for_quotation": data.parent
"request_for_quotation": data.parent,
"supplier_part_no": frappe.db.get_value("Item Supplier",
{'parent': data.item_code, 'supplier': supplier}, "supplier_part_no")
})
sq_doc.append('items', args)
@frappe.whitelist()
def get_pdf(doctype, name, supplier):
doc = get_rfq_doc(doctype, name, supplier)
@ -391,7 +393,7 @@ def get_item_from_material_requests_based_on_supplier(source_name, target_doc =
def get_supplier_tag():
if not frappe.cache().hget("Supplier", "Tags"):
filters = {"document_type": "Supplier"}
tags = list(set([tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag]))
tags = list(set(tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag))
frappe.cache().hset("Supplier", "Tags", tags)
return frappe.cache().hget("Supplier", "Tags")

View File

@ -0,0 +1,45 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Subcontract Order Summary"] = {
"filters": [
{
label: __("Company"),
fieldname: "company",
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1
},
{
label: __("From Date"),
fieldname:"from_date",
fieldtype: "Date",
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
reqd: 1
},
{
label: __("To Date"),
fieldname:"to_date",
fieldtype: "Date",
default: frappe.datetime.get_today(),
reqd: 1
},
{
label: __("Purchase Order"),
fieldname: "name",
fieldtype: "Link",
options: "Purchase Order",
get_query: function() {
return {
filters: {
docstatus: 1,
is_subcontracted: 'Yes',
company: frappe.query_report.get_filter_value('company')
}
}
}
}
]
};

View File

@ -0,0 +1,32 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-05-31 14:43:32.417694",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-05-31 14:43:32.417694",
"modified_by": "Administrator",
"module": "Buying",
"name": "Subcontract Order Summary",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Purchase Order",
"report_name": "Subcontract Order Summary",
"report_type": "Script Report",
"roles": [
{
"role": "Stock User"
},
{
"role": "Purchase Manager"
},
{
"role": "Purchase User"
}
]
}

View File

@ -0,0 +1,152 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
def execute(filters=None):
columns, data = [], []
columns = get_columns()
data = get_data(filters)
return columns, data
def get_data(report_filters):
data = []
orders = get_subcontracted_orders(report_filters)
if orders:
supplied_items = get_supplied_items(orders, report_filters)
po_details = prepare_subcontracted_data(orders, supplied_items)
get_subcontracted_data(po_details, data)
return data
def get_subcontracted_orders(report_filters):
fields = ['`tabPurchase Order Item`.`parent` as po_id', '`tabPurchase Order Item`.`item_code`',
'`tabPurchase Order Item`.`item_name`', '`tabPurchase Order Item`.`qty`', '`tabPurchase Order Item`.`name`',
'`tabPurchase Order Item`.`received_qty`', '`tabPurchase Order`.`status`']
filters = get_filters(report_filters)
return frappe.get_all('Purchase Order', fields = fields, filters=filters) or []
def get_filters(report_filters):
filters = [['Purchase Order', 'docstatus', '=', 1], ['Purchase Order', 'is_subcontracted', '=', 'Yes'],
['Purchase Order', 'transaction_date', 'between', (report_filters.from_date, report_filters.to_date)]]
for field in ['name', 'company']:
if report_filters.get(field):
filters.append(['Purchase Order', field, '=', report_filters.get(field)])
return filters
def get_supplied_items(orders, report_filters):
if not orders:
return []
fields = ['parent', 'main_item_code', 'rm_item_code', 'required_qty',
'supplied_qty', 'returned_qty', 'total_supplied_qty', 'consumed_qty', 'reference_name']
filters = {'parent': ('in', [d.po_id for d in orders]), 'docstatus': 1}
supplied_items = {}
for row in frappe.get_all('Purchase Order Item Supplied', fields = fields, filters=filters):
new_key = (row.parent, row.reference_name, row.main_item_code)
supplied_items.setdefault(new_key, []).append(row)
return supplied_items
def prepare_subcontracted_data(orders, supplied_items):
po_details = {}
for row in orders:
key = (row.po_id, row.name, row.item_code)
if key not in po_details:
po_details.setdefault(key, frappe._dict({'po_item': row, 'supplied_items': []}))
details = po_details[key]
if supplied_items.get(key):
for supplied_item in supplied_items[key]:
details['supplied_items'].append(supplied_item)
return po_details
def get_subcontracted_data(po_details, data):
for key, details in po_details.items():
res = details.po_item
for index, row in enumerate(details.supplied_items):
if index != 0:
res = {}
res.update(row)
data.append(res)
def get_columns():
return [
{
"label": _("Purchase Order"),
"fieldname": "po_id",
"fieldtype": "Link",
"options": "Purchase Order",
"width": 100
},
{
"label": _("Status"),
"fieldname": "status",
"fieldtype": "Data",
"width": 80
},
{
"label": _("Subcontracted Item"),
"fieldname": "item_code",
"fieldtype": "Link",
"options": "Item",
"width": 160
},
{
"label": _("Order Qty"),
"fieldname": "qty",
"fieldtype": "Float",
"width": 90
},
{
"label": _("Received Qty"),
"fieldname": "received_qty",
"fieldtype": "Float",
"width": 110
},
{
"label": _("Supplied Item"),
"fieldname": "rm_item_code",
"fieldtype": "Link",
"options": "Item",
"width": 160
},
{
"label": _("Required Qty"),
"fieldname": "required_qty",
"fieldtype": "Float",
"width": 110
},
{
"label": _("Supplied Qty"),
"fieldname": "supplied_qty",
"fieldtype": "Float",
"width": 110
},
{
"label": _("Consumed Qty"),
"fieldname": "consumed_qty",
"fieldtype": "Float",
"width": 120
},
{
"label": _("Returned Qty"),
"fieldname": "returned_qty",
"fieldtype": "Float",
"width": 110
}
]

View File

@ -9,10 +9,10 @@ def execute(filters=None):
if filters.from_date >= filters.to_date:
frappe.msgprint(_("To Date must be greater than From Date"))
data = []
columns = get_columns()
get_data(data , filters)
return columns, data
data = get_data(filters)
return columns, data or []
def get_columns():
return [
@ -21,13 +21,12 @@ def get_columns():
"fieldtype": "Link",
"fieldname": "purchase_order",
"options": "Purchase Order",
"width": 150
"width": 200
},
{
"label": _("Date"),
"fieldtype": "Date",
"fieldname": "date",
"hidden": 1,
"width": 150
},
{
@ -41,97 +40,58 @@ def get_columns():
"label": _("Item Code"),
"fieldtype": "Data",
"fieldname": "rm_item_code",
"width": 100
"width": 150
},
{
"label": _("Required Quantity"),
"fieldtype": "Float",
"fieldname": "r_qty",
"width": 100
"fieldname": "reqd_qty",
"width": 150
},
{
"label": _("Transferred Quantity"),
"fieldtype": "Float",
"fieldname": "t_qty",
"width": 100
"fieldname": "transferred_qty",
"width": 200
},
{
"label": _("Pending Quantity"),
"fieldtype": "Float",
"fieldname": "p_qty",
"width": 100
"width": 150
}
]
def get_data(data, filters):
po = get_po(filters)
po_transferred_qty_map = frappe._dict(get_transferred_quantity([v.name for v in po]))
def get_data(filters):
po_rm_item_details = get_po_items_to_supply(filters)
sub_items = get_purchase_order_item_supplied([v.name for v in po])
data = []
for row in po_rm_item_details:
transferred_qty = row.get("transferred_qty") or 0
if transferred_qty < row.get("reqd_qty", 0):
pending_qty = frappe.utils.flt(row.get("reqd_qty", 0) - transferred_qty)
row.p_qty = pending_qty if pending_qty > 0 else 0
data.append(row)
for order in po:
for item in sub_items:
if order.name == item.parent and order.name in po_transferred_qty_map and \
item.required_qty != po_transferred_qty_map.get(order.name).get(item.rm_item_code):
transferred_qty = po_transferred_qty_map.get(order.name).get(item.rm_item_code) \
if po_transferred_qty_map.get(order.name).get(item.rm_item_code) else 0
row ={
'purchase_order': item.parent,
'date': order.transaction_date,
'supplier': order.supplier,
'rm_item_code': item.rm_item_code,
'r_qty': item.required_qty,
't_qty':transferred_qty,
'p_qty':item.required_qty - transferred_qty
}
return data
data.append(row)
return(data)
def get_po(filters):
record_filters = [
["is_subcontracted", "=", "Yes"],
["supplier", "=", filters.supplier],
["transaction_date", "<=", filters.to_date],
["transaction_date", ">=", filters.from_date],
["docstatus", "=", 1]
]
return frappe.get_all("Purchase Order", filters=record_filters, fields=["name", "transaction_date", "supplier"])
def get_transferred_quantity(po_name):
stock_entries = get_stock_entry(po_name)
stock_entries_detail = get_stock_entry_detail([v.name for v in stock_entries])
po_transferred_qty_map = {}
for entry in stock_entries:
for details in stock_entries_detail:
if details.parent == entry.name:
details["Purchase_order"] = entry.purchase_order
if entry.purchase_order not in po_transferred_qty_map:
po_transferred_qty_map[entry.purchase_order] = {}
po_transferred_qty_map[entry.purchase_order][details.item_code] = details.qty
else:
po_transferred_qty_map[entry.purchase_order][details.item_code] = po_transferred_qty_map[entry.purchase_order].get(details.item_code, 0) + details.qty
return po_transferred_qty_map
def get_stock_entry(po):
return frappe.get_all("Stock Entry", filters=[
('purchase_order', 'IN', po),
('stock_entry_type', '=', 'Send to Subcontractor'),
('docstatus', '=', 1)
], fields=["name", "purchase_order"])
def get_stock_entry_detail(se):
return frappe.get_all("Stock Entry Detail", filters=[
["parent", "in", se]
def get_po_items_to_supply(filters):
return frappe.db.get_all(
"Purchase Order",
fields=[
"name as purchase_order",
"transaction_date as date",
"supplier as supplier",
"`tabPurchase Order Item Supplied`.rm_item_code as rm_item_code",
"`tabPurchase Order Item Supplied`.required_qty as reqd_qty",
"`tabPurchase Order Item Supplied`.supplied_qty as transferred_qty"
],
fields=["parent", "item_code", "qty"])
def get_purchase_order_item_supplied(po):
return frappe.get_all("Purchase Order Item Supplied", filters=[
('parent', 'IN', po)
], fields=['parent', 'rm_item_code', 'required_qty'])
filters = [
["Purchase Order", "per_received", "<", "100"],
["Purchase Order", "is_subcontracted", "=", "Yes"],
["Purchase Order", "supplier", "=", filters.supplier],
["Purchase Order", "transaction_date", "<=", filters.to_date],
["Purchase Order", "transaction_date", ">=", filters.from_date],
["Purchase Order", "docstatus", "=", 1]
]
)

View File

@ -12,34 +12,80 @@ import json, frappe, unittest
class TestSubcontractedItemToBeTransferred(unittest.TestCase):
def test_pending_and_transferred_qty(self):
po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes')
po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes', supplier_warehouse="_Test Warehouse 1 - _TC")
# Material Receipt of RMs
make_stock_entry(item_code='_Test Item', target='_Test Warehouse - _TC', qty=100, basic_rate=100)
make_stock_entry(item_code='_Test Item Home Desktop 100', target='_Test Warehouse - _TC', qty=100, basic_rate=100)
transfer_subcontracted_raw_materials(po.name)
col, data = execute(filters=frappe._dict({'supplier': po.supplier,
'from_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=-10)),
'to_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10))}))
self.assertEqual(data[0]['purchase_order'], po.name)
self.assertIn(data[0]['rm_item_code'], ['_Test Item', '_Test Item Home Desktop 100'])
self.assertIn(data[0]['p_qty'], [9, 18])
self.assertIn(data[0]['t_qty'], [1, 2])
self.assertEqual(data[1]['purchase_order'], po.name)
self.assertIn(data[1]['rm_item_code'], ['_Test Item', '_Test Item Home Desktop 100'])
self.assertIn(data[1]['p_qty'], [9, 18])
self.assertIn(data[1]['t_qty'], [1, 2])
se = transfer_subcontracted_raw_materials(po)
col, data = execute(filters=frappe._dict(
{
'supplier': po.supplier,
'from_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=-10)),
'to_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10))
}
))
po.reload()
po_data = [row for row in data if row.get('purchase_order') == po.name]
# Alphabetically sort to be certain of order
po_data = sorted(po_data, key = lambda i: i['rm_item_code'])
self.assertEqual(len(po_data), 2)
self.assertEqual(po_data[0]['purchase_order'], po.name)
self.assertEqual(po_data[0]['rm_item_code'], '_Test Item')
self.assertEqual(po_data[0]['p_qty'], 8)
self.assertEqual(po_data[0]['transferred_qty'], 2)
self.assertEqual(po_data[1]['rm_item_code'], '_Test Item Home Desktop 100')
self.assertEqual(po_data[1]['p_qty'], 19)
self.assertEqual(po_data[1]['transferred_qty'], 1)
se.cancel()
po.cancel()
def transfer_subcontracted_raw_materials(po):
# Order of supplied items fetched in PO is flaky
transfer_qty_map = {
'_Test Item': 2,
'_Test Item Home Desktop 100': 1
}
item_1 = po.supplied_items[0].rm_item_code
item_2 = po.supplied_items[1].rm_item_code
rm_item = [
{'item_code': '_Test Item', 'rm_item_code': '_Test Item', 'item_name': '_Test Item', 'qty': 1,
'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 100, 'stock_uom': 'Nos'},
{'item_code': '_Test Item Home Desktop 100', 'rm_item_code': '_Test Item Home Desktop 100', 'item_name': '_Test Item Home Desktop 100', 'qty': 2,
'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 200, 'stock_uom': 'Nos'}]
{
'name': po.supplied_items[0].name,
'item_code': item_1,
'rm_item_code': item_1,
'item_name': item_1,
'qty': transfer_qty_map[item_1],
'warehouse': '_Test Warehouse - _TC',
'rate': 100,
'amount': 100 * transfer_qty_map[item_1],
'stock_uom': 'Nos'
},
{
'name': po.supplied_items[1].name,
'item_code': item_2,
'rm_item_code': item_2,
'item_name': item_2,
'qty': transfer_qty_map[item_2],
'warehouse': '_Test Warehouse - _TC',
'rate': 100,
'amount': 100 * transfer_qty_map[item_2],
'stock_uom': 'Nos'
}
]
rm_item_string = json.dumps(rm_item)
se = frappe.get_doc(make_rm_stock_entry(po, rm_item_string))
se.from_warehouse = '_Test Warehouse 1 - _TC'
se.to_warehouse = '_Test Warehouse 1 - _TC'
se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string))
se.from_warehouse = '_Test Warehouse - _TC'
se.to_warehouse = '_Test Warehouse - _TC'
se.stock_entry_type = 'Send to Subcontractor'
se.save()
se.submit()
return se

View File

@ -0,0 +1,72 @@
# Version 13.6.0 Release Notes
### Features & Enhancements
- Job Card Enhancements ([#24523](https://github.com/frappe/erpnext/pull/24523))
- Implement multi-account selection in General Ledger([#26044](https://github.com/frappe/erpnext/pull/26044))
- Fetching of qty as per received qty from PR to PI ([#26184](https://github.com/frappe/erpnext/pull/26184))
- Subcontract code refactor and enhancement ([#25878](https://github.com/frappe/erpnext/pull/25878))
- Employee Grievance ([#25705](https://github.com/frappe/erpnext/pull/25705))
- Add Inactive status to Employee ([#26030](https://github.com/frappe/erpnext/pull/26030))
- Incorrect valuation rate report for serialized items ([#25696](https://github.com/frappe/erpnext/pull/25696))
- Update cost updates operation time and hour rates in BOM ([#25891](https://github.com/frappe/erpnext/pull/25891))
### Fixes
- Precision rate for packed items in internal transfers ([#26046](https://github.com/frappe/erpnext/pull/26046))
- User is not able to change item tax template ([#26176](https://github.com/frappe/erpnext/pull/26176))
- Insufficient permission for Dunning error ([#26092](https://github.com/frappe/erpnext/pull/26092))
- Validate Product Bundle for existing transactions before deletion ([#25978](https://github.com/frappe/erpnext/pull/25978))
- Auto unlink warehouse from item on delete ([#26073](https://github.com/frappe/erpnext/pull/26073))
- Employee Inactive status implications ([#26245](https://github.com/frappe/erpnext/pull/26245))
- Fetch batch items in stock reconciliation ([#26230](https://github.com/frappe/erpnext/pull/26230))
- Disabled cancellation for sales order if linked to drafted sales invoice ([#26125](https://github.com/frappe/erpnext/pull/26125))
- Sort website products by weightage mentioned in Item master ([#26134](https://github.com/frappe/erpnext/pull/26134))
- Added freeze when trying to stop work order (#26192) ([#26196](https://github.com/frappe/erpnext/pull/26196))
- Accounting Dimensions for payroll entry accrual Journal Entry ([#26083](https://github.com/frappe/erpnext/pull/26083))
- Staffing plan vacancies data type issue ([#25941](https://github.com/frappe/erpnext/pull/25941))
- Unable to enter score in Assessment Result details grid ([#25945](https://github.com/frappe/erpnext/pull/25945))
- Report Subcontracted Raw Materials to be Transferred ([#26011](https://github.com/frappe/erpnext/pull/26011))
- Label for enabling ledger posting of change amount ([#26070](https://github.com/frappe/erpnext/pull/26070))
- Training event ([#26071](https://github.com/frappe/erpnext/pull/26071))
- Rate not able to change in purchase order ([#26122](https://github.com/frappe/erpnext/pull/26122))
- Error while fetching item taxes ([#26220](https://github.com/frappe/erpnext/pull/26220))
- Check for duplicate payment terms in Payment Term Template ([#26003](https://github.com/frappe/erpnext/pull/26003))
- Removed values out of sync validation from stock transactions ([#26229](https://github.com/frappe/erpnext/pull/26229))
- Fetching employee in payroll entry ([#26269](https://github.com/frappe/erpnext/pull/26269))
- Filter Cost Center and Project drop-down lists by Company ([#26045](https://github.com/frappe/erpnext/pull/26045))
- Website item group logic for product listing in Item Group pages ([#26170](https://github.com/frappe/erpnext/pull/26170))
- Chart not visible for First Response Time reports ([#26032](https://github.com/frappe/erpnext/pull/26032))
- Incorrect billed qty in Sales Order analytics ([#26095](https://github.com/frappe/erpnext/pull/26095))
- Material request and supplier quotation not linked if supplier quotation created from supplier portal ([#26023](https://github.com/frappe/erpnext/pull/26023))
- Update leave allocation after submit ([#26191](https://github.com/frappe/erpnext/pull/26191))
- Taxes on Internal Transfer payment entry ([#26188](https://github.com/frappe/erpnext/pull/26188))
- Precision rate for packed items (bp #26046) ([#26217](https://github.com/frappe/erpnext/pull/26217))
- Fixed rounding off ordered percent to 100 in condition ([#26152](https://github.com/frappe/erpnext/pull/26152))
- Sanctioned loan amount limit check ([#26108](https://github.com/frappe/erpnext/pull/26108))
- Purchase receipt gl entries with same item code ([#26202](https://github.com/frappe/erpnext/pull/26202))
- Taxable value for invoices with additional discount ([#25906](https://github.com/frappe/erpnext/pull/25906))
- Correct South Africa VAT Rate (Updated) ([#25894](https://github.com/frappe/erpnext/pull/25894))
- Remove response_by and resolution_by if sla is removed ([#25997](https://github.com/frappe/erpnext/pull/25997))
- POS loyalty card alignment ([#26051](https://github.com/frappe/erpnext/pull/26051))
- Flaky test for Report Subcontracted Raw materials to be transferred ([#26043](https://github.com/frappe/erpnext/pull/26043))
- Export invoices not visible in GSTR-1 report ([#26143](https://github.com/frappe/erpnext/pull/26143))
- Account filter not working with accounting dimension filter ([#26211](https://github.com/frappe/erpnext/pull/26211))
- Allow to select group warehouse while downloading materials from production plan ([#26126](https://github.com/frappe/erpnext/pull/26126))
- Added freeze when trying to stop work order ([#26192](https://github.com/frappe/erpnext/pull/26192))
- Time out while submit / cancel the stock transactions with more than 50 Items ([#26081](https://github.com/frappe/erpnext/pull/26081))
- Address Card issues in e-commerce ([#26187](https://github.com/frappe/erpnext/pull/26187))
- Error while booking deferred revenue ([#26195](https://github.com/frappe/erpnext/pull/26195))
- Eliminate repeat creation of HSN codes ([#25947](https://github.com/frappe/erpnext/pull/25947))
- Opening invoices can alter profit and loss of a closed year ([#25951](https://github.com/frappe/erpnext/pull/25951))
- Payroll entry employee detail issue ([#25968](https://github.com/frappe/erpnext/pull/25968))
- Auto tax calculations in Payment Entry ([#26037](https://github.com/frappe/erpnext/pull/26037))
- Use pos invoice item name as unique identifier ([#26198](https://github.com/frappe/erpnext/pull/26198))
- Billing address not fetched in Purchase Invoice ([#26100](https://github.com/frappe/erpnext/pull/26100))
- Timeout while cancelling stock reconciliation ([#26098](https://github.com/frappe/erpnext/pull/26098))
- Status indicator for delivery notes ([#26062](https://github.com/frappe/erpnext/pull/26062))
- Unable to enter score in Assessment Result details grid ([#26031](https://github.com/frappe/erpnext/pull/26031))
- Too many writes while renaming company abbreviation ([#26203](https://github.com/frappe/erpnext/pull/26203))
- Chart not visible for First Response Time reports ([#26185](https://github.com/frappe/erpnext/pull/26185))
- Job applicant link issue ([#25934](https://github.com/frappe/erpnext/pull/25934))
- Fetch preferred shipping address (bp #26132) ([#26201](https://github.com/frappe/erpnext/pull/26201))

View File

@ -610,8 +610,8 @@ class AccountsController(TransactionBase):
order_field = "purchase_order"
order_doctype = "Purchase Order"
order_list = list(set([d.get(order_field)
for d in self.get("items") if d.get(order_field)]))
order_list = list(set(d.get(order_field)
for d in self.get("items") if d.get(order_field)))
journal_entries = get_advance_journal_entries(party_type, party, party_account,
amount_field, order_doctype, order_list, include_unallocated)
@ -635,8 +635,8 @@ class AccountsController(TransactionBase):
def validate_advance_entries(self):
order_field = "sales_order" if self.doctype == "Sales Invoice" else "purchase_order"
order_list = list(set([d.get(order_field)
for d in self.get("items") if d.get(order_field)]))
order_list = list(set(d.get(order_field)
for d in self.get("items") if d.get(order_field)))
if not order_list: return
@ -828,8 +828,14 @@ class AccountsController(TransactionBase):
role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill')
if total_billed_amt - max_allowed_amt > 0.01 and role_allowed_to_over_bill not in frappe.get_roles():
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
.format(item.item_code, item.idx, max_allowed_amt))
if self.doctype != "Purchase Invoice":
self.throw_overbill_exception(item, max_allowed_amt)
elif not cint(frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice")):
self.throw_overbill_exception(item, max_allowed_amt)
def throw_overbill_exception(self, item, max_allowed_amt):
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
.format(item.item_code, item.idx, max_allowed_amt))
def get_company_default(self, fieldname):
from erpnext.accounts.utils import get_company_default

View File

@ -11,16 +11,17 @@ from erpnext.accounts.party import get_party_details
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.buying.utils import validate_for_items, update_last_purchase_rate
from erpnext.stock.stock_ledger import get_valuation_rate
from erpnext.stock.doctype.stock_entry.stock_entry import get_used_alternative_items
from erpnext.stock.doctype.serial_no.serial_no import get_auto_serial_nos, auto_make_serial_nos, get_serial_nos
from frappe.contacts.doctype.address.address import get_address_display
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
from erpnext.controllers.stock_controller import StockController
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
from erpnext.stock.utils import get_incoming_rate
class BuyingController(StockController):
from erpnext.controllers.stock_controller import StockController
from erpnext.controllers.subcontracting import Subcontracting
class BuyingController(StockController, Subcontracting):
def get_feed(self):
if self.get("supplier_name"):
@ -57,6 +58,11 @@ class BuyingController(StockController):
if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
self.update_valuation_rate()
def onload(self):
super(BuyingController, self).onload()
self.set_onload("backflush_based_on", frappe.db.get_single_value('Buying Settings',
'backflush_raw_materials_of_subcontract_based_on'))
def set_missing_values(self, for_validate=False):
super(BuyingController, self).set_missing_values(for_validate)
@ -171,18 +177,19 @@ class BuyingController(StockController):
TODO: rename item_tax_amount to valuation_tax_amount
"""
stock_and_asset_items = []
stock_and_asset_items = self.get_stock_items() + self.get_asset_items()
stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0
last_item_idx = 1
for d in self.get("items"):
if d.item_code and d.item_code in stock_and_asset_items:
if (d.item_code and d.item_code in stock_and_asset_items):
stock_and_asset_items_qty += flt(d.qty)
stock_and_asset_items_amount += flt(d.base_net_amount)
last_item_idx = d.idx
total_valuation_amount = sum([flt(d.base_tax_amount_after_discount_amount) for d in self.get("taxes")
if d.category in ["Valuation", "Valuation and Total"]])
total_valuation_amount = sum(flt(d.base_tax_amount_after_discount_amount) for d in self.get("taxes")
if d.category in ["Valuation", "Valuation and Total"])
valuation_amount_adjustment = total_valuation_amount
for i, item in enumerate(self.get("items")):
@ -255,7 +262,7 @@ class BuyingController(StockController):
supplied_items_cost = 0.0
for d in self.get("supplied_items"):
if d.reference_name == item_row_id:
if reset_outgoing_rate and frappe.db.get_value('Item', d.rm_item_code, 'is_stock_item'):
if reset_outgoing_rate and frappe.get_cached_value('Item', d.rm_item_code, 'is_stock_item'):
rate = get_incoming_rate({
"item_code": d.rm_item_code,
"warehouse": self.supplier_warehouse,
@ -285,11 +292,13 @@ class BuyingController(StockController):
if item in self.sub_contracted_items and not item.bom:
frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code))
if self.doctype == "Purchase Order":
for supplied_item in self.get("supplied_items"):
if not supplied_item.reserve_warehouse:
frappe.throw(_("Reserved Warehouse is mandatory for Item {0} in Raw Materials supplied").format(frappe.bold(supplied_item.rm_item_code)))
if self.doctype != "Purchase Order":
return
for row in self.get("supplied_items"):
if not row.reserve_warehouse:
msg = f"Reserved Warehouse is mandatory for the Item {frappe.bold(row.rm_item_code)} in Raw Materials supplied"
frappe.throw(_(msg))
else:
for item in self.get("items"):
if item.bom:
@ -297,23 +306,7 @@ class BuyingController(StockController):
def create_raw_materials_supplied(self, raw_material_table):
if self.is_subcontracted=="Yes":
parent_items = []
backflush_raw_materials_based_on = frappe.db.get_single_value("Buying Settings",
"backflush_raw_materials_of_subcontract_based_on")
if (self.doctype == 'Purchase Receipt' and
backflush_raw_materials_based_on != 'BOM'):
self.update_raw_materials_supplied_based_on_stock_entries()
else:
for item in self.get("items"):
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
item.rm_supp_cost = 0.0
if item.bom and item.item_code in self.sub_contracted_items:
self.update_raw_materials_supplied_based_on_bom(item, raw_material_table)
if [item.item_code, item.name] not in parent_items:
parent_items.append([item.item_code, item.name])
self.cleanup_raw_materials_supplied(parent_items, raw_material_table)
self.set_materials_for_subcontracted_items(raw_material_table)
elif self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
for item in self.get("items"):
@ -322,176 +315,6 @@ class BuyingController(StockController):
if self.is_subcontracted == "No" and self.get("supplied_items"):
self.set('supplied_items', [])
def update_raw_materials_supplied_based_on_stock_entries(self):
self.set('supplied_items', [])
purchase_orders = set([d.purchase_order for d in self.items])
# qty of raw materials backflushed (for each item per purchase order)
backflushed_raw_materials_map = get_backflushed_subcontracted_raw_materials(purchase_orders)
# qty of "finished good" item yet to be received
qty_to_be_received_map = get_qty_to_be_received(purchase_orders)
for item in self.get('items'):
if not item.purchase_order:
continue
# reset raw_material cost
item.rm_supp_cost = 0
# qty of raw materials transferred to the supplier
transferred_raw_materials = get_subcontracted_raw_materials_from_se(item.purchase_order, item.item_code)
non_stock_items = get_non_stock_items(item.purchase_order, item.item_code)
item_key = '{}{}'.format(item.item_code, item.purchase_order)
fg_yet_to_be_received = qty_to_be_received_map.get(item_key)
if not fg_yet_to_be_received:
frappe.throw(_("Row #{0}: Item {1} is already fully received in Purchase Order {2}")
.format(item.idx, frappe.bold(item.item_code),
frappe.utils.get_link_to_form("Purchase Order", item.purchase_order)),
title=_("Limit Crossed"))
transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code)
# backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code)
for raw_material in transferred_raw_materials + non_stock_items:
rm_item_key = (raw_material.rm_item_code, item.item_code, item.purchase_order)
raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {})
consumed_qty = raw_material_data.get('qty', 0)
consumed_serial_nos = raw_material_data.get('serial_no', '')
consumed_batch_nos = raw_material_data.get('batch_nos', '')
transferred_qty = raw_material.qty
rm_qty_to_be_consumed = transferred_qty - consumed_qty
# backflush all remaining transferred qty in the last Purchase Receipt
if fg_yet_to_be_received == item.qty:
qty = rm_qty_to_be_consumed
else:
qty = (rm_qty_to_be_consumed / fg_yet_to_be_received) * item.qty
if frappe.get_cached_value('UOM', raw_material.stock_uom, 'must_be_whole_number'):
qty = frappe.utils.ceil(qty)
if qty > rm_qty_to_be_consumed:
qty = rm_qty_to_be_consumed
if not qty: continue
if raw_material.serial_nos:
set_serial_nos(raw_material, consumed_serial_nos, qty)
if raw_material.batch_nos:
backflushed_batch_qty_map = raw_material_data.get('consumed_batch', {})
batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code,
qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order)
for batch_data in batches_qty:
qty = batch_data['qty']
raw_material.batch_no = batch_data['batch']
if qty > 0:
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
else:
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
def append_raw_material_to_be_backflushed(self, fg_item_row, raw_material_data, qty):
rm = self.append('supplied_items', {})
rm.update(raw_material_data)
if not rm.main_item_code:
rm.main_item_code = fg_item_row.item_code
rm.reference_name = fg_item_row.name
rm.required_qty = qty
rm.consumed_qty = qty
def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table):
exploded_item = 1
if hasattr(item, 'include_exploded_items'):
exploded_item = item.get('include_exploded_items')
bom_items = get_items_from_bom(item.item_code, item.bom, exploded_item)
used_alternative_items = []
if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order:
used_alternative_items = get_used_alternative_items(purchase_order = item.purchase_order)
raw_materials_cost = 0
items = list(set([d.item_code for d in bom_items]))
item_wh = frappe._dict(frappe.db.sql("""select i.item_code, id.default_warehouse
from `tabItem` i, `tabItem Default` id
where id.parent=i.name and id.company=%s and i.name in ({0})"""
.format(", ".join(["%s"] * len(items))), [self.company] + items))
for bom_item in bom_items:
if self.doctype == "Purchase Order":
reserve_warehouse = bom_item.source_warehouse or item_wh.get(bom_item.item_code)
if frappe.db.get_value("Warehouse", reserve_warehouse, "company") != self.company:
reserve_warehouse = None
conversion_factor = item.conversion_factor
if (self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order and
bom_item.item_code in used_alternative_items):
alternative_item_data = used_alternative_items.get(bom_item.item_code)
bom_item.item_code = alternative_item_data.item_code
bom_item.item_name = alternative_item_data.item_name
bom_item.stock_uom = alternative_item_data.stock_uom
conversion_factor = alternative_item_data.conversion_factor
bom_item.description = alternative_item_data.description
# check if exists
exists = 0
for d in self.get(raw_material_table):
if d.main_item_code == item.item_code and d.rm_item_code == bom_item.item_code \
and d.reference_name == item.name:
rm, exists = d, 1
break
if not exists:
rm = self.append(raw_material_table, {})
required_qty = flt(flt(bom_item.qty_consumed_per_unit) * (flt(item.qty) + getattr(item, 'rejected_qty', 0)) *
flt(conversion_factor), rm.precision("required_qty"))
rm.reference_name = item.name
rm.bom_detail_no = bom_item.name
rm.main_item_code = item.item_code
rm.rm_item_code = bom_item.item_code
rm.stock_uom = bom_item.stock_uom
rm.required_qty = required_qty
rm.rate = bom_item.rate
rm.conversion_factor = conversion_factor
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
rm.consumed_qty = required_qty
rm.description = bom_item.description
if item.batch_no and frappe.db.get_value("Item", rm.rm_item_code, "has_batch_no") and not rm.batch_no:
rm.batch_no = item.batch_no
elif not rm.reserve_warehouse:
rm.reserve_warehouse = reserve_warehouse
def cleanup_raw_materials_supplied(self, parent_items, raw_material_table):
"""Remove all those child items which are no longer present in main item table"""
delete_list = []
for d in self.get(raw_material_table):
if [d.main_item_code, d.reference_name] not in parent_items:
# mark for deletion from doclist
delete_list.append(d)
# delete from doclist
if delete_list:
rm_supplied_details = self.get(raw_material_table)
self.set(raw_material_table, [])
for d in rm_supplied_details:
if d not in delete_list:
self.append(raw_material_table, d)
@property
def sub_contracted_items(self):
if not hasattr(self, "_sub_contracted_items"):
@ -683,7 +506,8 @@ class BuyingController(StockController):
self.process_fixed_asset()
self.update_fixed_asset(field)
update_last_purchase_rate(self, is_submit = 1)
if self.doctype in ['Purchase Order', 'Purchase Receipt']:
update_last_purchase_rate(self, is_submit = 1)
def on_cancel(self):
super(BuyingController, self).on_cancel()
@ -691,7 +515,9 @@ class BuyingController(StockController):
if self.get('is_return'):
return
update_last_purchase_rate(self, is_submit = 0)
if self.doctype in ['Purchase Order', 'Purchase Receipt']:
update_last_purchase_rate(self, is_submit = 0)
if self.doctype in ['Purchase Receipt', 'Purchase Invoice']:
field = 'purchase_invoice' if self.doctype == 'Purchase Invoice' else 'purchase_receipt'
@ -863,104 +689,6 @@ class BuyingController(StockController):
else:
validate_item_type(self, "is_purchase_item", "purchase")
def get_items_from_bom(item_code, bom, exploded_item=1):
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
bom_items = frappe.db.sql("""select t2.item_code, t2.name,
t2.rate, t2.stock_uom, t2.source_warehouse, t2.description,
t2.stock_qty / ifnull(t1.quantity, 1) as qty_consumed_per_unit
from
`tabBOM` t1, `tab{0}` t2, tabItem t3
where
t2.parent = t1.name and t1.item = %s
and t1.docstatus = 1 and t1.is_active = 1 and t1.name = %s
and t2.sourced_by_supplier = 0
and t2.item_code = t3.name""".format(doctype),
(item_code, bom), as_dict=1)
if not bom_items:
msgprint(_("Specified BOM {0} does not exist for Item {1}").format(bom, item_code), raise_exception=1)
return bom_items
def get_subcontracted_raw_materials_from_se(purchase_order, fg_item):
common_query = """
SELECT
sed.item_code AS rm_item_code,
SUM(sed.qty) AS qty,
sed.description,
sed.stock_uom,
sed.subcontracted_item AS main_item_code,
{serial_no_concat_syntax} AS serial_nos,
{batch_no_concat_syntax} AS batch_nos
FROM `tabStock Entry` se,`tabStock Entry Detail` sed
WHERE
se.name = sed.parent
AND se.docstatus=1
AND se.purpose='Send to Subcontractor'
AND se.purchase_order = %s
AND IFNULL(sed.t_warehouse, '') != ''
AND IFNULL(sed.subcontracted_item, '') in ('', %s)
GROUP BY sed.item_code, sed.subcontracted_item
"""
raw_materials = frappe.db.multisql({
'mariadb': common_query.format(
serial_no_concat_syntax="GROUP_CONCAT(sed.serial_no)",
batch_no_concat_syntax="GROUP_CONCAT(sed.batch_no)"
),
'postgres': common_query.format(
serial_no_concat_syntax="STRING_AGG(sed.serial_no, ',')",
batch_no_concat_syntax="STRING_AGG(sed.batch_no, ',')"
)
}, (purchase_order, fg_item), as_dict=1)
return raw_materials
def get_backflushed_subcontracted_raw_materials(purchase_orders):
purchase_receipts = frappe.get_all("Purchase Receipt Item",
fields = ["purchase_order", "item_code", "name", "parent"],
filters={"docstatus": 1, "purchase_order": ("in", list(purchase_orders))})
distinct_purchase_receipts = {}
for pr in purchase_receipts:
key = (pr.purchase_order, pr.item_code, pr.parent)
distinct_purchase_receipts.setdefault(key, []).append(pr.name)
backflushed_raw_materials_map = frappe._dict()
for args, references in iteritems(distinct_purchase_receipts):
purchase_receipt_supplied_items = get_supplied_items(args[1], args[2], references)
for data in purchase_receipt_supplied_items:
pr_key = (data.rm_item_code, data.main_item_code, args[0])
if pr_key not in backflushed_raw_materials_map:
backflushed_raw_materials_map.setdefault(pr_key, frappe._dict({
"qty": 0.0,
"serial_no": [],
"batch_no": [],
"consumed_batch": {}
}))
row = backflushed_raw_materials_map.get(pr_key)
row.qty += data.consumed_qty
for field in ["serial_no", "batch_no"]:
if data.get(field):
row[field].append(data.get(field))
if data.get("batch_no"):
if data.get("batch_no") in row.consumed_batch:
row.consumed_batch[data.get("batch_no")] += data.consumed_qty
else:
row.consumed_batch[data.get("batch_no")] = data.consumed_qty
return backflushed_raw_materials_map
def get_supplied_items(item_code, purchase_receipt, references):
return frappe.get_all("Purchase Receipt Item Supplied",
fields=["rm_item_code", "main_item_code", "consumed_qty", "serial_no", "batch_no"],
filters={"main_item_code": item_code, "parent": purchase_receipt, "reference_name": ("in", references)})
def get_asset_item_details(asset_items):
asset_items_data = {}
for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"],
@ -992,135 +720,3 @@ def validate_item_type(doc, fieldname, message):
error_message = _("Following item {0} is not marked as {1} item. You can enable them as {1} item from its Item master").format(items, message)
frappe.throw(error_message)
def get_qty_to_be_received(purchase_orders):
return frappe._dict(frappe.db.sql("""
SELECT CONCAT(poi.`item_code`, poi.`parent`) AS item_key,
SUM(poi.`qty`) - SUM(poi.`received_qty`) AS qty_to_be_received
FROM `tabPurchase Order Item` poi
WHERE
poi.`parent` in %s
GROUP BY poi.`item_code`, poi.`parent`
HAVING SUM(poi.`qty`) > SUM(poi.`received_qty`)
""", (purchase_orders)))
def get_non_stock_items(purchase_order, fg_item_code):
return frappe.db.sql("""
SELECT
pois.main_item_code,
pois.rm_item_code,
item.description,
pois.required_qty AS qty,
pois.rate,
1 as non_stock_item,
pois.stock_uom
FROM `tabPurchase Order Item Supplied` pois, `tabItem` item
WHERE
pois.`rm_item_code` = item.`name`
AND item.is_stock_item = 0
AND pois.`parent` = %s
AND pois.`main_item_code` = %s
""", (purchase_order, fg_item_code), as_dict=1)
def set_serial_nos(raw_material, consumed_serial_nos, qty):
serial_nos = set(get_serial_nos(raw_material.serial_nos)) - \
set(get_serial_nos(consumed_serial_nos))
if serial_nos and qty <= len(serial_nos):
raw_material.serial_no = '\n'.join(list(serial_nos)[0:frappe.utils.cint(qty)])
def get_transferred_batch_qty_map(purchase_order, fg_item):
# returns
# {
# (item_code, fg_code): {
# batch1: 10, # qty
# batch2: 16
# },
# }
transferred_batch_qty_map = {}
transferred_batches = frappe.db.sql("""
SELECT
sed.batch_no,
SUM(sed.qty) AS qty,
sed.item_code,
sed.subcontracted_item
FROM `tabStock Entry` se,`tabStock Entry Detail` sed
WHERE
se.name = sed.parent
AND se.docstatus=1
AND se.purpose='Send to Subcontractor'
AND se.purchase_order = %s
AND ifnull(sed.subcontracted_item, '') in ('', %s)
AND sed.batch_no IS NOT NULL
GROUP BY
sed.batch_no,
sed.item_code
""", (purchase_order, fg_item), as_dict=1)
for batch_data in transferred_batches:
key = ((batch_data.item_code, fg_item)
if batch_data.subcontracted_item else (batch_data.item_code, purchase_order))
transferred_batch_qty_map.setdefault(key, OrderedDict())
transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty
return transferred_batch_qty_map
def get_backflushed_batch_qty_map(purchase_order, fg_item):
# returns
# {
# (item_code, fg_code): {
# batch1: 10, # qty
# batch2: 16
# },
# }
backflushed_batch_qty_map = {}
backflushed_batches = frappe.db.sql("""
SELECT
pris.batch_no,
SUM(pris.consumed_qty) AS qty,
pris.rm_item_code AS item_code
FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` pris
WHERE
pr.name = pri.parent
AND pri.parent = pris.parent
AND pri.purchase_order = %s
AND pri.item_code = pris.main_item_code
AND pr.docstatus = 1
AND pris.main_item_code = %s
AND pris.batch_no IS NOT NULL
GROUP BY
pris.rm_item_code, pris.batch_no
""", (purchase_order, fg_item), as_dict=1)
for batch_data in backflushed_batches:
backflushed_batch_qty_map.setdefault((batch_data.item_code, fg_item), {})
backflushed_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty
return backflushed_batch_qty_map
def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batches, po):
# Returns available batches to be backflushed based on requirements
transferred_batches = transferred_batch_qty_map.get((item_code, fg_item), {})
if not transferred_batches:
transferred_batches = transferred_batch_qty_map.get((item_code, po), {})
available_batches = []
for (batch, transferred_qty) in transferred_batches.items():
backflushed_qty = backflushed_batches.get(batch, 0)
available_qty = transferred_qty - backflushed_qty
if available_qty >= required_qty:
available_batches.append({'batch': batch, 'qty': required_qty})
break
elif available_qty != 0:
available_batches.append({'batch': batch, 'qty': available_qty})
required_qty -= available_qty
for row in available_batches:
if backflushed_batches.get(row.get('batch'), 0) > 0:
backflushed_batches[row.get('batch')] += row.get('qty')
else:
backflushed_batches[row.get('batch')] = row.get('qty')
return available_batches

View File

@ -19,7 +19,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
fields = get_fields("Employee", ["name", "employee_name"])
return frappe.db.sql("""select {fields} from `tabEmployee`
where status = 'Active'
where status in ('Active', 'Suspended')
and docstatus < 2
and ({key} like %(txt)s
or employee_name like %(txt)s)
@ -88,7 +88,7 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters):
fields = get_fields("Customer", fields)
searchfields = frappe.get_meta("Customer").get_search_fields()
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
searchfields = " or ".join(field + " like %(txt)s" for field in searchfields)
return frappe.db.sql("""select {fields} from `tabCustomer`
where docstatus < 2
@ -315,7 +315,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql("""select {fields} from `tabProject`
where
`tabProject`.status not in ("Completed", "Cancelled")
and {cond} {match_cond} {scond}
and {cond} {scond} {match_cond}
order by
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
idx desc,

View File

@ -330,9 +330,15 @@ class SellingController(StockController):
# For internal transfers use incoming rate as the valuation rate
if self.is_internal_transfer():
rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate'))
if d.rate != rate:
d.rate = rate
if d.doctype == "Packed Item":
incoming_rate = flt(d.incoming_rate * d.conversion_factor, d.precision('incoming_rate'))
if d.incoming_rate != incoming_rate:
d.incoming_rate = incoming_rate
else:
rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate'))
if d.rate != rate:
d.rate = rate
d.discount_percentage = 0
d.discount_amount = 0
frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer")
@ -428,7 +434,7 @@ class SellingController(StockController):
self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(','))))
def get_po_nos(self, ref_doctype, ref_fieldname, po_nos):
doc_list = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)]))
doc_list = list(set(d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)))
if doc_list:
po_nos += [d.po_no for d in frappe.get_all(ref_doctype, 'po_no', filters = {'name': ('in', doc_list)}) if d.get('po_no')]

View File

@ -299,8 +299,8 @@ class StatusUpdater(Document):
args['name'] = self.get(args['percent_join_field_parent'])
self._update_percent_field(args, update_modified)
else:
distinct_transactions = set([d.get(args['percent_join_field'])
for d in self.get_all_children(args['source_dt'])])
distinct_transactions = set(d.get(args['percent_join_field'])
for d in self.get_all_children(args['source_dt']))
for name in distinct_transactions:
if name:

View File

@ -11,7 +11,7 @@ from frappe.utils import cint, cstr, flt, get_link_to_form, getdate
import erpnext
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map
from erpnext.accounts.utils import check_if_stock_and_account_balance_synced, get_fiscal_year
from erpnext.accounts.utils import get_fiscal_year
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.stock_ledger import get_valuation_rate
@ -313,7 +313,7 @@ class StockController(AccountsController):
def get_serialized_items(self):
serialized_items = []
item_codes = list(set([d.item_code for d in self.get("items")]))
item_codes = list(set(d.item_code for d in self.get("items")))
if item_codes:
serialized_items = frappe.db.sql_list("""select name from `tabItem`
where has_serial_no=1 and name in ({})""".format(", ".join(["%s"]*len(item_codes))),
@ -324,8 +324,8 @@ class StockController(AccountsController):
def validate_warehouse(self):
from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company
warehouses = list(set([d.warehouse for d in
self.get("items") if getattr(d, "warehouse", None)]))
warehouses = list(set(d.warehouse for d in
self.get("items") if getattr(d, "warehouse", None)))
target_warehouses = list(set([d.target_warehouse for d in
self.get("items") if getattr(d, "target_warehouse", None)]))
@ -497,10 +497,6 @@ class StockController(AccountsController):
})
if future_sle_exists(args):
create_repost_item_valuation_entry(args)
elif not is_reposting_pending():
check_if_stock_and_account_balance_synced(self.posting_date,
self.company, self.doctype, self.name)
@frappe.whitelist()
def make_quality_inspections(doctype, docname, items):
@ -533,21 +529,75 @@ def make_quality_inspections(doctype, docname, items):
return inspections
def is_reposting_pending():
return frappe.db.exists("Repost Item Valuation",
{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
def future_sle_exists(args, sl_entries=None):
key = (args.voucher_type, args.voucher_no)
def future_sle_exists(args):
sl_entries = frappe.get_all("Stock Ledger Entry",
if validate_future_sle_not_exists(args, key, sl_entries):
return False
elif get_cached_data(args, key):
return True
if not sl_entries:
sl_entries = get_sle_entries_against_voucher(args)
if not sl_entries:
return
or_conditions = get_conditions_to_validate_future_sle(sl_entries)
data = frappe.db.sql("""
select item_code, warehouse, count(name) as total_row
from `tabStock Ledger Entry`
where
({})
and timestamp(posting_date, posting_time)
>= timestamp(%(posting_date)s, %(posting_time)s)
and voucher_no != %(voucher_no)s
and is_cancelled = 0
GROUP BY
item_code, warehouse
""".format(" or ".join(or_conditions)), args, as_dict=1)
for d in data:
frappe.local.future_sle[key][(d.item_code, d.warehouse)] = d.total_row
return len(data)
def validate_future_sle_not_exists(args, key, sl_entries=None):
item_key = ''
if args.get('item_code'):
item_key = (args.get('item_code'), args.get('warehouse'))
if not sl_entries and hasattr(frappe.local, 'future_sle'):
if (not frappe.local.future_sle.get(key) or
(item_key and item_key not in frappe.local.future_sle.get(key))):
return True
def get_cached_data(args, key):
if not hasattr(frappe.local, 'future_sle'):
frappe.local.future_sle = {}
if key not in frappe.local.future_sle:
frappe.local.future_sle[key] = frappe._dict({})
if args.get('item_code'):
item_key = (args.get('item_code'), args.get('warehouse'))
count = frappe.local.future_sle[key].get(item_key)
return True if (count or count == 0) else False
else:
return frappe.local.future_sle[key]
def get_sle_entries_against_voucher(args):
return frappe.get_all("Stock Ledger Entry",
filters={"voucher_type": args.voucher_type, "voucher_no": args.voucher_no},
fields=["item_code", "warehouse"],
order_by="creation asc")
if not sl_entries:
return
def get_conditions_to_validate_future_sle(sl_entries):
warehouse_items_map = {}
for entry in sl_entries:
if entry.warehouse not in warehouse_items_map:
@ -558,23 +608,10 @@ def future_sle_exists(args):
or_conditions = []
for warehouse, items in warehouse_items_map.items():
or_conditions.append(
"warehouse = '{}' and item_code in ({})".format(
warehouse,
", ".join(frappe.db.escape(item) for item in items)
)
)
f"""warehouse = {frappe.db.escape(warehouse)}
and item_code in ({', '.join(frappe.db.escape(item) for item in items)})""")
return frappe.db.sql("""
select name
from `tabStock Ledger Entry`
where
({})
and timestamp(posting_date, posting_time)
>= timestamp(%(posting_date)s, %(posting_time)s)
and voucher_no != %(voucher_no)s
and is_cancelled = 0
limit 1
""".format(" or ".join(or_conditions)), args)
return or_conditions
def create_repost_item_valuation_entry(args):
args = frappe._dict(args)

View File

@ -0,0 +1,393 @@
import frappe
import copy
from frappe import _
from frappe.utils import flt, cint, get_link_to_form
from collections import defaultdict
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
class Subcontracting():
def set_materials_for_subcontracted_items(self, raw_material_table):
if self.doctype == 'Purchase Invoice' and not self.update_stock:
return
self.raw_material_table = raw_material_table
self.__identify_change_in_item_table()
self.__prepare_supplied_items()
self.__validate_supplied_items()
def __prepare_supplied_items(self):
self.initialized_fields()
self.__get_purchase_orders()
self.__get_pending_qty_to_receive()
self.get_available_materials()
self.__remove_changed_rows()
self.__set_supplied_items()
def initialized_fields(self):
self.available_materials = frappe._dict()
self.__transferred_items = frappe._dict()
self.alternative_item_details = frappe._dict()
self.__get_backflush_based_on()
def __get_backflush_based_on(self):
self.backflush_based_on = frappe.db.get_single_value("Buying Settings",
"backflush_raw_materials_of_subcontract_based_on")
def __get_purchase_orders(self):
self.purchase_orders = []
if self.doctype == 'Purchase Order':
return
self.purchase_orders = [d.purchase_order for d in self.items if d.purchase_order]
def __identify_change_in_item_table(self):
self.__changed_name = []
self.__reference_name = []
if self.doctype == 'Purchase Order' or self.is_new():
self.set(self.raw_material_table, [])
return
item_dict = self.__get_data_before_save()
if not item_dict:
return True
for n_row in self.items:
self.__reference_name.append(n_row.name)
if (n_row.name not in item_dict) or (n_row.item_code, n_row.qty) != item_dict[n_row.name]:
self.__changed_name.append(n_row.name)
if item_dict.get(n_row.name):
del item_dict[n_row.name]
self.__changed_name.extend(item_dict.keys())
def __get_data_before_save(self):
item_dict = {}
if self.doctype in ['Purchase Receipt', 'Purchase Invoice'] and self._doc_before_save:
for row in self._doc_before_save.get('items'):
item_dict[row.name] = (row.item_code, row.qty)
return item_dict
def get_available_materials(self):
''' Get the available raw materials which has been transferred to the supplier.
available_materials = {
(item_code, subcontracted_item, purchase_order): {
'qty': 1, 'serial_no': [ABC], 'batch_no': {'batch1': 1}, 'data': item_details
}
}
'''
if not self.purchase_orders:
return
for row in self.__get_transferred_items():
key = (row.rm_item_code, row.main_item_code, row.purchase_order)
if key not in self.available_materials:
self.available_materials.setdefault(key, frappe._dict({'qty': 0, 'serial_no': [],
'batch_no': defaultdict(float), 'item_details': row, 'po_details': []})
)
details = self.available_materials[key]
details.qty += row.qty
details.po_details.append(row.po_detail)
if row.serial_no:
details.serial_no.extend(get_serial_nos(row.serial_no))
if row.batch_no:
details.batch_no[row.batch_no] += row.qty
self.__set_alternative_item_details(row)
self.__transferred_items = copy.deepcopy(self.available_materials)
for doctype in ['Purchase Receipt', 'Purchase Invoice']:
self.__update_consumed_materials(doctype)
def __update_consumed_materials(self, doctype, return_consumed_items=False):
'''Deduct the consumed materials from the available materials.'''
pr_items = self.__get_received_items(doctype)
if not pr_items:
return ([], {}) if return_consumed_items else None
pr_items = {d.name: d.get(self.get('po_field') or 'purchase_order') for d in pr_items}
consumed_materials = self.__get_consumed_items(doctype, pr_items.keys())
if return_consumed_items:
return (consumed_materials, pr_items)
for row in consumed_materials:
key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name))
if not self.available_materials.get(key):
continue
self.available_materials[key]['qty'] -= row.consumed_qty
if row.serial_no:
self.available_materials[key]['serial_no'] = list(
set(self.available_materials[key]['serial_no']) - set(get_serial_nos(row.serial_no))
)
if row.batch_no:
self.available_materials[key]['batch_no'][row.batch_no] -= row.consumed_qty
def __get_transferred_items(self):
fields = ['`tabStock Entry`.`purchase_order`']
alias_dict = {'item_code': 'rm_item_code', 'subcontracted_item': 'main_item_code', 'basic_rate': 'rate'}
child_table_fields = ['item_code', 'item_name', 'description', 'qty', 'basic_rate', 'amount',
'serial_no', 'uom', 'subcontracted_item', 'stock_uom', 'batch_no', 'conversion_factor',
's_warehouse', 't_warehouse', 'item_group', 'po_detail']
if self.backflush_based_on == 'BOM':
child_table_fields.append('original_item')
for field in child_table_fields:
fields.append(f'`tabStock Entry Detail`.`{field}` As {alias_dict.get(field, field)}')
filters = [['Stock Entry', 'docstatus', '=', 1], ['Stock Entry', 'purpose', '=', 'Send to Subcontractor'],
['Stock Entry', 'purchase_order', 'in', self.purchase_orders]]
return frappe.get_all('Stock Entry', fields = fields, filters=filters)
def __get_received_items(self, doctype):
fields = []
self.po_field = 'purchase_order'
for field in ['name', self.po_field, 'parent']:
fields.append(f'`tab{doctype} Item`.`{field}`')
filters = [[doctype, 'docstatus', '=', 1], [f'{doctype} Item', self.po_field, 'in', self.purchase_orders]]
if doctype == 'Purchase Invoice':
filters.append(['Purchase Invoice', 'update_stock', "=", 1])
return frappe.get_all(f'{doctype}', fields = fields, filters = filters)
def __get_consumed_items(self, doctype, pr_items):
return frappe.get_all('Purchase Receipt Item Supplied',
fields = ['serial_no', 'rm_item_code', 'reference_name', 'batch_no', 'consumed_qty', 'main_item_code'],
filters = {'docstatus': 1, 'reference_name': ('in', list(pr_items)), 'parenttype': doctype})
def __set_alternative_item_details(self, row):
if row.get('original_item'):
self.alternative_item_details[row.get('original_item')] = row
def __get_pending_qty_to_receive(self):
'''Get qty to be received against the purchase order.'''
self.qty_to_be_received = defaultdict(float)
if self.doctype != 'Purchase Order' and self.backflush_based_on != 'BOM' and self.purchase_orders:
for row in frappe.get_all('Purchase Order Item',
fields = ['item_code', '(qty - received_qty) as qty', 'parent', 'name'],
filters = {'docstatus': 1, 'parent': ('in', self.purchase_orders)}):
self.qty_to_be_received[(row.item_code, row.parent)] += row.qty
def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
doctype = 'BOM Item' if not exploded_item else 'BOM Explosion Item'
fields = [f'`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit']
alias_dict = {'item_code': 'rm_item_code', 'name': 'bom_detail_no', 'source_warehouse': 'reserve_warehouse'}
for field in ['item_code', 'name', 'rate', 'stock_uom',
'source_warehouse', 'description', 'item_name', 'stock_uom']:
fields.append(f'`tab{doctype}`.`{field}` As {alias_dict.get(field, field)}')
filters = [[doctype, 'parent', '=', bom_no], [doctype, 'docstatus', '=', 1],
['BOM', 'item', '=', item_code], [doctype, 'sourced_by_supplier', '=', 0]]
return frappe.get_all('BOM', fields = fields, filters=filters, order_by = f'`tab{doctype}`.`idx`') or []
def __remove_changed_rows(self):
if not self.__changed_name:
return
i=1
self.set(self.raw_material_table, [])
for d in self._doc_before_save.supplied_items:
if d.reference_name in self.__changed_name:
continue
if (d.reference_name not in self.__reference_name):
continue
d.idx = i
self.append('supplied_items', d)
i += 1
def __set_supplied_items(self):
self.bom_items = {}
has_supplied_items = True if self.get(self.raw_material_table) else False
for row in self.items:
if (self.doctype != 'Purchase Order' and ((self.__changed_name and row.name not in self.__changed_name)
or (has_supplied_items and not self.__changed_name))):
continue
if self.doctype == 'Purchase Order' or self.backflush_based_on == 'BOM':
for bom_item in self.__get_materials_from_bom(row.item_code, row.bom, row.get('include_exploded_items')):
qty = (flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor)
bom_item.main_item_code = row.item_code
self.__update_reserve_warehouse(bom_item, row)
self.__set_alternative_item(bom_item)
self.__add_supplied_item(row, bom_item, qty)
elif self.backflush_based_on != 'BOM':
for key, transfer_item in self.available_materials.items():
if (key[1], key[2]) == (row.item_code, row.purchase_order) and transfer_item.qty > 0:
qty = self.__get_qty_based_on_material_transfer(row, transfer_item) or 0
transfer_item.qty -= qty
self.__add_supplied_item(row, transfer_item.get('item_details'), qty)
if self.qty_to_be_received:
self.qty_to_be_received[(row.item_code, row.purchase_order)] -= row.qty
def __update_reserve_warehouse(self, row, item):
if self.doctype == 'Purchase Order':
row.reserve_warehouse = (self.set_reserve_warehouse or item.warehouse)
def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
key = (item_row.item_code, item_row.purchase_order)
if self.qty_to_be_received == item_row.qty:
return transfer_item.qty
if self.qty_to_be_received:
qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0))
transfer_item.item_details.required_qty = transfer_item.qty
if (transfer_item.serial_no or frappe.get_cached_value('UOM',
transfer_item.item_details.stock_uom, 'must_be_whole_number')):
return frappe.utils.ceil(qty)
return qty
def __set_alternative_item(self, bom_item):
if self.alternative_item_details.get(bom_item.rm_item_code):
bom_item.update(self.alternative_item_details[bom_item.rm_item_code])
def __add_supplied_item(self, item_row, bom_item, qty):
bom_item.conversion_factor = item_row.conversion_factor
rm_obj = self.append(self.raw_material_table, bom_item)
rm_obj.reference_name = item_row.name
if self.doctype == 'Purchase Order':
rm_obj.required_qty = qty
else:
rm_obj.consumed_qty = 0
rm_obj.purchase_order = item_row.purchase_order
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
def __set_batch_nos(self, bom_item, item_row, rm_obj, qty):
key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order)
if (self.available_materials.get(key) and self.available_materials[key]['batch_no']):
new_rm_obj = None
for batch_no, batch_qty in self.available_materials[key]['batch_no'].items():
if batch_qty >= qty:
self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty)
self.available_materials[key]['batch_no'][batch_no] -= qty
return
elif qty > 0 and batch_qty > 0:
qty -= batch_qty
new_rm_obj = self.append(self.raw_material_table, bom_item)
new_rm_obj.reference_name = item_row.name
self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty)
self.available_materials[key]['batch_no'][batch_no] = 0
if abs(qty) > 0 and not new_rm_obj:
self.__set_consumed_qty(rm_obj, qty)
else:
self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty)
self.__set_serial_nos(item_row, rm_obj)
def __set_consumed_qty(self, rm_obj, consumed_qty, required_qty=0):
rm_obj.required_qty = required_qty
rm_obj.consumed_qty = consumed_qty
def __set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty):
rm_obj.update({'consumed_qty': qty, 'batch_no': batch_no,
'required_qty': qty, 'purchase_order': item_row.purchase_order})
self.__set_serial_nos(item_row, rm_obj)
def __set_serial_nos(self, item_row, rm_obj):
key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order)
if (self.available_materials.get(key) and self.available_materials[key]['serial_no']):
used_serial_nos = self.available_materials[key]['serial_no'][0: cint(rm_obj.consumed_qty)]
rm_obj.serial_no = '\n'.join(used_serial_nos)
# Removed the used serial nos from the list
for sn in used_serial_nos:
self.available_materials[key]['serial_no'].remove(sn)
def set_consumed_qty_in_po(self):
# Update consumed qty back in the purchase order
if self.is_subcontracted != 'Yes':
return
self.__get_purchase_orders()
itemwise_consumed_qty = defaultdict(float)
for doctype in ['Purchase Receipt', 'Purchase Invoice']:
consumed_items, pr_items = self.__update_consumed_materials(doctype, return_consumed_items=True)
for row in consumed_items:
key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name))
itemwise_consumed_qty[key] += row.consumed_qty
self.__update_consumed_qty_in_po(itemwise_consumed_qty)
def __update_consumed_qty_in_po(self, itemwise_consumed_qty):
fields = ['main_item_code', 'rm_item_code', 'parent', 'supplied_qty', 'name']
filters = {'docstatus': 1, 'parent': ('in', self.purchase_orders)}
for row in frappe.get_all('Purchase Order Item Supplied', fields = fields, filters=filters, order_by='idx'):
key = (row.rm_item_code, row.main_item_code, row.parent)
consumed_qty = itemwise_consumed_qty.get(key, 0)
if row.supplied_qty < consumed_qty:
consumed_qty = row.supplied_qty
itemwise_consumed_qty[key] -= consumed_qty
frappe.db.set_value('Purchase Order Item Supplied', row.name, 'consumed_qty', consumed_qty)
def __validate_supplied_items(self):
if self.doctype not in ['Purchase Invoice', 'Purchase Receipt']:
return
for row in self.get(self.raw_material_table):
self.__validate_consumed_qty(row)
key = (row.rm_item_code, row.main_item_code, row.purchase_order)
if not self.__transferred_items or not self.__transferred_items.get(key):
return
self.__validate_batch_no(row, key)
self.__validate_serial_no(row, key)
def __validate_consumed_qty(self, row):
if self.backflush_based_on != 'BOM' and flt(row.consumed_qty) == 0.0:
msg = f'Row {row.idx}: the consumed qty cannot be zero for the item {frappe.bold(row.rm_item_code)}'
frappe.throw(_(msg),title=_('Consumed Items Qty Check'))
def __validate_batch_no(self, row, key):
if row.get('batch_no') and row.get('batch_no') not in self.__transferred_items.get(key).get('batch_no'):
link = get_link_to_form('Purchase Order', row.purchase_order)
msg = f'The Batch No {frappe.bold(row.get("batch_no"))} has not supplied against the Purchase Order {link}'
frappe.throw(_(msg), title=_("Incorrect Batch Consumed"))
def __validate_serial_no(self, row, key):
if row.get('serial_no'):
serial_nos = get_serial_nos(row.get('serial_no'))
incorrect_sn = set(serial_nos).difference(self.__transferred_items.get(key).get('serial_no'))
if incorrect_sn:
incorrect_sn = "\n".join(incorrect_sn)
link = get_link_to_form('Purchase Order', row.purchase_order)
msg = f'The Serial Nos {incorrect_sn} has not supplied against the Purchase Order {link}'
frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed"))

View File

@ -378,10 +378,10 @@ class calculate_taxes_and_totals(object):
def manipulate_grand_total_for_inclusive_tax(self):
# if fully inclusive taxes and diff
if self.doc.get("taxes") and any([cint(t.included_in_print_rate) for t in self.doc.get("taxes")]):
if self.doc.get("taxes") and any(cint(t.included_in_print_rate) for t in self.doc.get("taxes")):
last_tax = self.doc.get("taxes")[-1]
non_inclusive_tax_amount = sum([flt(d.tax_amount_after_discount_amount)
for d in self.doc.get("taxes") if not d.included_in_print_rate])
non_inclusive_tax_amount = sum(flt(d.tax_amount_after_discount_amount)
for d in self.doc.get("taxes") if not d.included_in_print_rate)
diff = self.doc.total + non_inclusive_tax_amount \
- flt(last_tax.total, last_tax.precision("total"))
@ -521,8 +521,8 @@ class calculate_taxes_and_totals(object):
def calculate_total_advance(self):
if self.doc.docstatus < 2:
total_allocated_amount = sum([flt(adv.allocated_amount, adv.precision("allocated_amount"))
for adv in self.doc.get("advances")])
total_allocated_amount = sum(flt(adv.allocated_amount, adv.precision("allocated_amount"))
for adv in self.doc.get("advances"))
self.doc.total_advance = flt(total_allocated_amount, self.doc.precision("total_advance"))
@ -622,7 +622,7 @@ class calculate_taxes_and_totals(object):
if self.doc.doctype == "Sales Invoice" \
and self.doc.paid_amount > self.doc.grand_total and not self.doc.is_return \
and any([d.type == "Cash" for d in self.doc.payments]):
and any(d.type == "Cash" for d in self.doc.payments):
grand_total = self.doc.rounded_total or self.doc.grand_total
base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total
@ -658,7 +658,13 @@ class calculate_taxes_and_totals(object):
item.margin_type = None
item.margin_rate_or_amount = 0.0
if item.margin_type and item.margin_rate_or_amount:
if not item.pricing_rules and flt(item.rate) > flt(item.price_list_rate):
item.margin_type = "Amount"
item.margin_rate_or_amount = flt(item.rate - item.price_list_rate,
item.precision("margin_rate_or_amount"))
item.rate_with_margin = item.rate
elif item.margin_type and item.margin_rate_or_amount:
margin_value = item.margin_rate_or_amount if item.margin_type == 'Amount' else flt(item.price_list_rate) * flt(item.margin_rate_or_amount) / 100
rate_with_margin = flt(item.price_list_rate) + flt(margin_value)
base_rate_with_margin = flt(rate_with_margin) * flt(self.doc.conversion_rate)

View File

@ -26,8 +26,8 @@ class TestMapper(unittest.TestCase):
# Assert that all inserted items are present in updated sales order
src_items = item_list_1 + item_list_2 + item_list_3
self.assertEqual(set([d for d in src_items]),
set([d.item_code for d in updated_so.items]))
self.assertEqual(set(d for d in src_items),
set(d.item_code for d in updated_so.items))
def make_quotation(self, item_list, customer):

View File

@ -113,7 +113,7 @@ def post_process(doctype, data):
doc.set_indicator()
doc.status_display = ", ".join(doc.status_display)
doc.items_preview = ", ".join([d.item_name for d in doc.items if d.item_name])
doc.items_preview = ", ".join(d.item_name for d in doc.items if d.item_name)
result.append(doc)
return result

View File

@ -22,10 +22,10 @@ frappe.query_reports["First Response Time for Opportunity"] = {
get_chart_data: function (_columns, result) {
return {
data: {
labels: result.map(d => d[0]),
labels: result.map(d => d.creation_date),
datasets: [{
name: "First Response Time",
values: result.map(d => d[1])
values: result.map(d => d.first_response_time)
}]
},
type: "line",
@ -35,8 +35,7 @@ frappe.query_reports["First Response Time for Opportunity"] = {
hide_days: 0,
hide_seconds: 0
};
value = frappe.utils.get_formatted_duration(d, duration_options);
return value;
return frappe.utils.get_formatted_duration(d, duration_options);
}
}
}

View File

@ -6,7 +6,8 @@ frappe.ui.form.on('Assessment Result', {
if (!frm.doc.__islocal) {
frm.trigger('setup_chart');
}
frm.set_df_property('details', 'read_only', 1);
frm.get_field('details').grid.cannot_add_rows = true;
frm.set_query('course', function() {
return {

View File

@ -33,21 +33,21 @@ class Patient(Document):
self.reload() # self.notify_update()
def on_update(self):
if self.customer:
customer = frappe.get_doc('Customer', self.customer)
if self.customer_group:
customer.customer_group = self.customer_group
if self.territory:
customer.territory = self.territory
if frappe.db.get_single_value('Healthcare Settings', 'link_customer_to_patient'):
if self.customer:
customer = frappe.get_doc('Customer', self.customer)
if self.customer_group:
customer.customer_group = self.customer_group
if self.territory:
customer.territory = self.territory
customer.customer_name = self.patient_name
customer.default_price_list = self.default_price_list
customer.default_currency = self.default_currency
customer.language = self.language
customer.ignore_mandatory = True
customer.save(ignore_permissions=True)
else:
if frappe.db.get_single_value('Healthcare Settings', 'link_customer_to_patient'):
customer.customer_name = self.patient_name
customer.default_price_list = self.default_price_list
customer.default_currency = self.default_currency
customer.language = self.language
customer.ignore_mandatory = True
customer.save(ignore_permissions=True)
else:
create_customer(self)
def set_full_name(self):

View File

@ -11,5 +11,5 @@ cur_frm.cscript.onload = function(doc, cdt, cdn) {
cur_frm.fields_dict.employee.get_query = function(doc,cdt,cdn) {
return{
query: "erpnext.controllers.queries.employee_query"
}
}
}

View File

@ -15,6 +15,7 @@ class Attendance(Document):
validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"])
self.validate_attendance_date()
self.validate_duplicate_record()
self.validate_employee_status()
self.check_leave_record()
def validate_attendance_date(self):
@ -38,6 +39,10 @@ class Attendance(Document):
frappe.throw(_("Attendance for employee {0} is already marked for the date {1}").format(
frappe.bold(self.employee), frappe.bold(self.attendance_date)))
def validate_employee_status(self):
if frappe.db.get_value("Employee", self.employee, "status") == "Inactive":
frappe.throw(_("Cannot mark attendance for an Inactive employee {0}").format(self.employee))
def check_leave_record(self):
leave_record = frappe.db.sql("""
select leave_type, half_day, half_day_date

View File

@ -21,6 +21,9 @@ frappe.listview_settings['Attendance'] = {
label: __('For Employee'),
fieldtype: 'Link',
options: 'Employee',
get_query: () => {
return {query: "erpnext.controllers.queries.employee_query"}
},
reqd: 1,
onchange: function() {
dialog.set_df_property("unmarked_days", "hidden", 1);

View File

@ -207,7 +207,7 @@
"label": "Status",
"oldfieldname": "status",
"oldfieldtype": "Select",
"options": "Active\nInactive\nLeft",
"options": "Active\nInactive\nSuspended\nLeft",
"reqd": 1,
"search_index": 1
},
@ -813,7 +813,7 @@
"idx": 24,
"image_field": "image",
"links": [],
"modified": "2021-06-12 11:31:37.730760",
"modified": "2021-06-17 11:31:37.730760",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee",

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import getdate, validate_email_address, today, add_years, format_datetime, cstr
from frappe.utils import getdate, validate_email_address, today, add_years, cstr
from frappe.model.naming import set_name_by_naming_series
from frappe import throw, _, scrub
from frappe.permissions import add_user_permission, remove_user_permission, \
@ -12,7 +12,6 @@ from frappe.permissions import add_user_permission, remove_user_permission, \
from frappe.model.document import Document
from erpnext.utilities.transaction_base import delete_events
from frappe.utils.nestedset import NestedSet
from erpnext.hr.doctype.job_offer.job_offer import get_staffing_plan_detail
class EmployeeUserDisabledError(frappe.ValidationError): pass
class EmployeeLeftValidationError(frappe.ValidationError): pass
@ -37,7 +36,7 @@ class Employee(NestedSet):
def validate(self):
from erpnext.controllers.status_updater import validate_status
validate_status(self.status, ["Active", "Inactive", "Left"])
validate_status(self.status, ["Active", "Inactive", "Suspended", "Left"])
self.employee = self.name
self.set_employee_name()

View File

@ -7,7 +7,8 @@ def get_data():
'heatmap_message': _('This is based on the attendance of this Employee'),
'fieldname': 'employee',
'non_standard_fieldnames': {
'Bank Account': 'party'
'Bank Account': 'party',
'Employee Grievance': 'raised_by'
},
'transactions': [
{
@ -20,7 +21,7 @@ def get_data():
},
{
'label': _('Lifecycle'),
'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation']
'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance']
},
{
'label': _('Shift'),

View File

@ -3,7 +3,7 @@ frappe.listview_settings['Employee'] = {
filters: [["status","=", "Active"]],
get_indicator: function(doc) {
var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status];
indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray"}[doc.status];
indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray", "Suspended": "orange"}[doc.status];
return indicator;
}
};

View File

@ -0,0 +1,39 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Employee Grievance', {
setup: function(frm) {
frm.set_query('grievance_against_party', function() {
return {
filters: {
name: ['in', [
'Company', 'Department', 'Employee Group', 'Employee Grade', 'Employee']
]
}
};
});
frm.set_query('associated_document_type', function() {
let ignore_modules = ["Setup", "Core", "Integrations", "Automation", "Website",
"Utilities", "Event Streaming", "Social", "Chat", "Data Migration", "Printing", "Desk", "Custom"];
return {
filters: {
istable: 0,
issingle: 0,
module: ["Not In", ignore_modules]
}
};
});
},
grievance_against_party: function(frm) {
let filters = {};
if (frm.doc.grievance_against_party == 'Employee' && frm.doc.raised_by) {
filters.name = ["!=", frm.doc.raised_by];
}
frm.set_query('grievance_against', function() {
return {
filters: filters
};
});
},
});

View File

@ -0,0 +1,261 @@
{
"actions": [],
"autoname": "HR-GRIEV-.YYYY.-.#####",
"creation": "2021-05-11 13:41:51.485295",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"subject",
"raised_by",
"employee_name",
"designation",
"column_break_3",
"date",
"status",
"reports_to",
"grievance_details_section",
"grievance_against_party",
"grievance_against",
"grievance_type",
"column_break_11",
"associated_document_type",
"associated_document",
"section_break_14",
"description",
"investigation_details_section",
"cause_of_grievance",
"resolution_details_section",
"resolved_by",
"resolution_date",
"employee_responsible",
"column_break_16",
"resolution_detail",
"amended_from"
],
"fields": [
{
"fieldname": "grievance_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Grievance Type",
"options": "Grievance Type",
"reqd": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Date ",
"reqd": 1
},
{
"default": "Open",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Open\nInvestigated\nResolved\nInvalid",
"reqd": 1
},
{
"fieldname": "description",
"fieldtype": "Text",
"label": "Description",
"reqd": 1
},
{
"fieldname": "cause_of_grievance",
"fieldtype": "Text",
"label": "Cause of Grievance",
"mandatory_depends_on": "eval: doc.status == \"Investigated\" || doc.status == \"Resolved\""
},
{
"fieldname": "resolution_details_section",
"fieldtype": "Section Break",
"label": "Resolution Details"
},
{
"fieldname": "resolved_by",
"fieldtype": "Link",
"label": "Resolved By",
"mandatory_depends_on": "eval: doc.status == \"Resolved\"",
"options": "User"
},
{
"fieldname": "employee_responsible",
"fieldtype": "Link",
"label": "Employee Responsible ",
"options": "Employee"
},
{
"fieldname": "resolution_detail",
"fieldtype": "Small Text",
"label": "Resolution Details",
"mandatory_depends_on": "eval: doc.status == \"Resolved\""
},
{
"fieldname": "column_break_16",
"fieldtype": "Column Break"
},
{
"fieldname": "resolution_date",
"fieldtype": "Date",
"label": "Resolution Date",
"mandatory_depends_on": "eval: doc.status == \"Resolved\""
},
{
"fieldname": "grievance_against",
"fieldtype": "Dynamic Link",
"label": "Grievance Against",
"options": "grievance_against_party",
"reqd": 1
},
{
"fieldname": "raised_by",
"fieldtype": "Link",
"label": "Raised By",
"options": "Employee",
"reqd": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Employee Grievance",
"print_hide": 1,
"read_only": 1
},
{
"fetch_from": "raised_by.designation",
"fieldname": "designation",
"fieldtype": "Link",
"label": "Designation",
"options": "Designation",
"read_only": 1
},
{
"fetch_from": "raised_by.reports_to",
"fieldname": "reports_to",
"fieldtype": "Link",
"label": "Reports To",
"options": "Employee",
"read_only": 1
},
{
"fieldname": "grievance_details_section",
"fieldtype": "Section Break",
"label": "Grievance Details"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_14",
"fieldtype": "Section Break"
},
{
"fieldname": "grievance_against_party",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Grievance Against Party",
"options": "DocType",
"reqd": 1
},
{
"fieldname": "associated_document_type",
"fieldtype": "Link",
"label": "Associated Document Type",
"options": "DocType"
},
{
"fieldname": "associated_document",
"fieldtype": "Dynamic Link",
"label": "Associated Document",
"options": "associated_document_type"
},
{
"fieldname": "investigation_details_section",
"fieldtype": "Section Break",
"label": "Investigation Details"
},
{
"fetch_from": "raised_by.employee_name",
"fieldname": "employee_name",
"fieldtype": "Data",
"label": "Employee Name",
"read_only": 1
},
{
"fieldname": "subject",
"fieldtype": "Data",
"label": "Subject",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-06-21 12:51:01.499486",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Grievance",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"select": 1,
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"select": 1,
"share": 1,
"submit": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"share": 1,
"write": 1
}
],
"search_fields": "subject,raised_by,grievance_against_party",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "subject",
"track_changes": 1
}

View File

@ -0,0 +1,15 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _, bold
from frappe.model.document import Document
class EmployeeGrievance(Document):
def on_submit(self):
if self.status not in ["Invalid", "Resolved"]:
frappe.throw(_("Only Employee Grievance with status {0} or {1} can be submitted").format(
bold("Invalid"),
bold("Resolved"))
)

View File

@ -0,0 +1,12 @@
frappe.listview_settings["Employee Grievance"] = {
has_indicator_for_draft: 1,
get_indicator: function(doc) {
var colors = {
"Open": "red",
"Investigated": "orange",
"Resolved": "green",
"Invalid": "grey"
};
return [__(doc.status), colors[doc.status], "status,=," + doc.status];
}
};

View File

@ -0,0 +1,51 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
import unittest
from frappe.utils import today
from erpnext.hr.doctype.employee.test_employee import make_employee
class TestEmployeeGrievance(unittest.TestCase):
def test_create_employee_grievance(self):
create_employee_grievance()
def create_employee_grievance():
grievance_type = create_grievance_type()
emp_1 = make_employee("test_emp_grievance_@example.com", company="_Test Company")
emp_2 = make_employee("testculprit@example.com", company="_Test Company")
grievance = frappe.new_doc("Employee Grievance")
grievance.subject = "Test Employee Grievance"
grievance.raised_by = emp_1
grievance.date = today()
grievance.grievance_type = grievance_type
grievance.grievance_against_party = "Employee"
grievance.grievance_against = emp_2
grievance.description = "test descrip"
#set cause
grievance.cause_of_grievance = "test cause"
#resolution details
grievance.resolution_date = today()
grievance.resolution_detail = "test resolution detail"
grievance.resolved_by = "test_emp_grievance_@example.com"
grievance.employee_responsible = emp_2
grievance.status = "Resolved"
grievance.save()
grievance.submit()
return grievance
def create_grievance_type():
if frappe.db.exists("Grievance Type", "Employee Abuse"):
return frappe.get_doc("Grievance Type", "Employee Abuse")
grievance_type = frappe.new_doc("Grievance Type")
grievance_type.name = "Employee Abuse"
grievance_type.description = "Test"
grievance_type.save()
return grievance_type.name

View File

@ -0,0 +1,8 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Grievance Type', {
// refresh: function(frm) {
// }
});

View File

@ -0,0 +1,70 @@
{
"actions": [],
"autoname": "Prompt",
"creation": "2021-05-11 12:41:50.256071",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"section_break_5",
"description"
],
"fields": [
{
"fieldname": "section_break_5",
"fieldtype": "Section Break"
},
{
"fieldname": "description",
"fieldtype": "Text",
"label": "Description"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-06-21 12:54:37.764712",
"modified_by": "Administrator",
"module": "HR",
"name": "Grievance Type",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class GrievanceType(Document):
pass

View File

@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
class TestGrievanceType(unittest.TestCase):
pass

View File

@ -2,7 +2,7 @@
// MIT License. See license.txt
frappe.listview_settings['Job Applicant'] = {
add_fields: ["company", "designation", "job_applicant", "status"],
add_fields: ["status"],
get_indicator: function (doc) {
if (doc.status == "Accepted") {
return [__(doc.status), "green", "status,=," + doc.status];

View File

@ -110,6 +110,7 @@
"label": "Allocation"
},
{
"allow_on_submit": 1,
"bold": 1,
"fieldname": "new_leaves_allocated",
"fieldtype": "Float",
@ -235,7 +236,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-04-14 15:28:26.335104",
"modified": "2021-06-03 15:28:26.335104",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Allocation",
@ -277,4 +278,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"timeline_field": "employee"
}
}

View File

@ -8,6 +8,7 @@ from frappe import _
from frappe.model.document import Document
from erpnext.hr.utils import set_employee_name, get_leave_period
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import expire_allocation, create_leave_ledger_entry
from erpnext.hr.doctype.leave_application.leave_application import get_approved_leaves_for_period
class OverlapError(frappe.ValidationError): pass
class BackDatedAllocationError(frappe.ValidationError): pass
@ -55,6 +56,43 @@ class LeaveAllocation(Document):
if self.carry_forward:
self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True)
def on_update_after_submit(self):
if self.has_value_changed("new_leaves_allocated"):
self.validate_against_leave_applications()
leaves_to_be_added = self.new_leaves_allocated - self.get_existing_leave_count()
args = {
"leaves": leaves_to_be_added,
"from_date": self.from_date,
"to_date": self.to_date,
"is_carry_forward": 0
}
create_leave_ledger_entry(self, args, True)
def get_existing_leave_count(self):
ledger_entries = frappe.get_all("Leave Ledger Entry",
filters={
"transaction_type": "Leave Allocation",
"transaction_name": self.name,
"employee": self.employee,
"company": self.company,
"leave_type": self.leave_type
},
pluck="leaves")
total_existing_leaves = 0
for entry in ledger_entries:
total_existing_leaves += entry
return total_existing_leaves
def validate_against_leave_applications(self):
leaves_taken = get_approved_leaves_for_period(self.employee, self.leave_type,
self.from_date, self.to_date)
if flt(leaves_taken) > flt(self.total_leaves_allocated):
if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
frappe.msgprint(_("Note: Total allocated leaves {0} shouldn't be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken))
else:
frappe.throw(_("Total allocated leaves {0} cannot be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken), LessAllocationError)
def update_leave_policy_assignments_when_no_allocations_left(self):
allocations = frappe.db.get_list("Leave Allocation", filters = {
"docstatus": 1,
@ -225,4 +263,4 @@ def get_unused_leaves(employee, leave_type, from_date, to_date):
def validate_carry_forward(leave_type):
if not frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"):
frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type))
frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type))

View File

@ -1,5 +1,6 @@
from __future__ import unicode_literals
import frappe
import erpnext
import unittest
from frappe.utils import nowdate, add_months, getdate, add_days
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
@ -164,6 +165,51 @@ class TestLeaveAllocation(unittest.TestCase):
leave_allocation.cancel()
self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name}))
def test_leave_addition_after_submit(self):
frappe.db.sql("delete from `tabLeave Allocation`")
frappe.db.sql("delete from `tabLeave Ledger Entry`")
leave_allocation = create_leave_allocation()
leave_allocation.submit()
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
leave_allocation.new_leaves_allocated = 40
leave_allocation.submit()
self.assertTrue(leave_allocation.total_leaves_allocated, 40)
def test_leave_subtraction_after_submit(self):
frappe.db.sql("delete from `tabLeave Allocation`")
frappe.db.sql("delete from `tabLeave Ledger Entry`")
leave_allocation = create_leave_allocation()
leave_allocation.submit()
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
leave_allocation.new_leaves_allocated = 10
leave_allocation.submit()
self.assertTrue(leave_allocation.total_leaves_allocated, 10)
def test_against_leave_application_validation_after_submit(self):
frappe.db.sql("delete from `tabLeave Allocation`")
frappe.db.sql("delete from `tabLeave Ledger Entry`")
leave_allocation = create_leave_allocation()
leave_allocation.submit()
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
leave_application = frappe.get_doc({
"doctype": 'Leave Application',
"employee": employee.name,
"leave_type": "_Test Leave Type",
"from_date": add_months(nowdate(), 2),
"to_date": add_months(add_days(nowdate(), 10), 2),
"company": erpnext.get_default_company() or "_Test Company",
"docstatus": 1,
"status": "Approved",
"leave_approver": 'test@example.com'
})
leave_application.submit()
leave_allocation.new_leaves_allocated = 8
leave_allocation.total_leaves_allocated = 8
self.assertRaises(frappe.ValidationError, leave_allocation.submit)
def create_leave_allocation(**args):
args = frappe._dict(args)

View File

@ -41,7 +41,7 @@ class StaffingPlan(Document):
detail.total_estimated_cost = 0
if detail.number_of_positions > 0:
if detail.vacancies > 0 and detail.estimated_cost_per_position:
if detail.vacancies and detail.estimated_cost_per_position:
detail.total_estimated_cost = cint(detail.vacancies) * flt(detail.estimated_cost_per_position)
self.total_estimated_budget += detail.total_estimated_cost
@ -76,12 +76,12 @@ class StaffingPlan(Document):
if cint(staffing_plan_detail.vacancies) > cint(parent_plan_details[0].vacancies) or \
flt(staffing_plan_detail.total_estimated_cost) > flt(parent_plan_details[0].total_estimated_cost):
frappe.throw(_("You can only plan for upto {0} vacancies and budget {1} \
for {2} as per staffing plan {3} for parent company {4}."
.format(cint(parent_plan_details[0].vacancies),
for {2} as per staffing plan {3} for parent company {4}.").format(
cint(parent_plan_details[0].vacancies),
parent_plan_details[0].total_estimated_cost,
frappe.bold(staffing_plan_detail.designation),
parent_plan_details[0].name,
parent_company)), ParentCompanyError)
parent_company), ParentCompanyError)
#Get vacanices already planned for all companies down the hierarchy of Parent Company
lft, rgt = frappe.get_cached_value('Company', parent_company, ["lft", "rgt"])
@ -98,14 +98,14 @@ class StaffingPlan(Document):
(flt(parent_plan_details[0].total_estimated_cost) < \
(flt(staffing_plan_detail.total_estimated_cost) + flt(all_sibling_details.total_estimated_cost))):
frappe.throw(_("{0} vacancies and {1} budget for {2} already planned for subsidiary companies of {3}. \
You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}."
.format(cint(all_sibling_details.vacancies),
You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}.").format(
cint(all_sibling_details.vacancies),
all_sibling_details.total_estimated_cost,
frappe.bold(staffing_plan_detail.designation),
parent_company,
cint(parent_plan_details[0].vacancies),
parent_plan_details[0].total_estimated_cost,
parent_plan_details[0].name)))
parent_plan_details[0].name))
def validate_with_subsidiary_plans(self, staffing_plan_detail):
#Valdate this plan with all child company plan
@ -121,11 +121,11 @@ class StaffingPlan(Document):
cint(staffing_plan_detail.vacancies) < cint(children_details.vacancies) or \
flt(staffing_plan_detail.total_estimated_cost) < flt(children_details.total_estimated_cost):
frappe.throw(_("Subsidiary companies have already planned for {1} vacancies at a budget of {2}. \
Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies"
.format(self.company,
Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies").format(
self.company,
cint(children_details.vacancies),
children_details.total_estimated_cost,
frappe.bold(staffing_plan_detail.designation))), SubsidiaryCompanyError)
frappe.bold(staffing_plan_detail.designation)), SubsidiaryCompanyError)
@frappe.whitelist()
def get_designation_counts(designation, company):
@ -170,4 +170,4 @@ def get_active_staffing_plan_details(company, designation, from_date=getdate(now
designation, from_date, to_date)
# Only a single staffing plan can be active for a designation on given date
return staffing_plan if staffing_plan else None
return staffing_plan if staffing_plan else None

View File

@ -11,8 +11,8 @@
"event": "Submit",
"idx": 0,
"is_standard": 1,
"message": "<table class=\"panel-header\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n <tr height=\"10\"></tr>\n <tr>\n <td width=\"15\"></td>\n <td>\n <div class=\"text-medium text-muted\">\n <span>{{_(\"Training Event:\")}} {{ doc.event_name }}</span>\n </div>\n </td>\n <td width=\"15\"></td>\n </tr>\n <tr height=\"10\"></tr>\n</table>\n\n<table class=\"panel-body\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n <tr height=\"10\"></tr>\n <tr>\n <td width=\"15\"></td>\n <td>\n <div>\n {{ doc.introduction }}\n <ul class=\"list-unstyled\" style=\"line-height: 1.7\">\n <li>{{_(\"Event Location\")}}: <b>{{ doc.location }}</b></li>\n {% set start = frappe.utils.get_datetime(doc.start_time) %}\n {% set end = frappe.utils.get_datetime(doc.end_time) %}\n {% if start.date() == end.date() %}\n <li>{{_(\"Date\")}}: <b>{{ start.strftime(\"%A, %d %b %Y\") }}</b></li>\n <li>\n {{_(\"Timing\")}}: <b>{{ start.strftime(\"%I:%M %p\") + ' to ' + end.strftime(\"%I:%M %p\") }}</b>\n </li>\n {% else %}\n <li>{{_(\"Start Time\")}}: <b>{{ start.strftime(\"%A, %d %b %Y at %I:%M %p\") }}</b>\n </li>\n <li>{{_(\"End Time\")}}: <b>{{ end.strftime(\"%A, %d %b %Y at %I:%M %p\") }}</b>\n </li>\n {% endif %}\n <li>{{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>\n {% if doc.is_mandatory %}\n <li>Note: This Training Event is mandatory</li>\n {% endif %}\n </ul>\n </div>\n </td>\n <td width=\"15\"></td>\n </tr>\n <tr height=\"10\"></tr>\n</table>",
"modified": "2021-05-24 16:29:13.165930",
"message": "<table class=\"panel-header\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n <tr height=\"10\"></tr>\n <tr>\n <td width=\"15\"></td>\n <td>\n <div class=\"text-medium text-muted\">\n <span>{{_(\"Training Event:\")}} {{ doc.event_name }}</span>\n </div>\n </td>\n <td width=\"15\"></td>\n </tr>\n <tr height=\"10\"></tr>\n</table>\n\n<table class=\"panel-body\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n <tr height=\"10\"></tr>\n <tr>\n <td width=\"15\"></td>\n <td>\n <div>\n {{ doc.introduction }}\n <ul class=\"list-unstyled\" style=\"line-height: 1.7\">\n <li>{{_(\"Event Location\")}}: <b>{{ doc.location }}</b></li>\n {% set start = frappe.utils.get_datetime(doc.start_time) %}\n {% set end = frappe.utils.get_datetime(doc.end_time) %}\n {% if start.date() == end.date() %}\n <li>{{_(\"Date\")}}: <b>{{ start.strftime(\"%A, %d %b %Y\") }}</b></li>\n <li>\n {{_(\"Timing\")}}: <b>{{ start.strftime(\"%I:%M %p\") + ' to ' + end.strftime(\"%I:%M %p\") }}</b>\n </li>\n {% else %}\n <li>\n {{_(\"Start Time\")}}: <b>{{ start.strftime(\"%A, %d %b %Y at %I:%M %p\") }}</b>\n </li>\n <li>{{_(\"End Time\")}}: <b>{{ end.strftime(\"%A, %d %b %Y at %I:%M %p\") }}</b></li>\n {% endif %}\n <li>{{ _(\"Event Link\") }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>\n {% if doc.is_mandatory %}\n <li>{{ _(\"Note: This Training Event is mandatory\") }}</li>\n {% endif %}\n </ul>\n </div>\n </td>\n <td width=\"15\"></td>\n </tr>\n <tr height=\"10\"></tr>\n</table>",
"modified": "2021-06-16 14:08:12.933367",
"modified_by": "Administrator",
"module": "HR",
"name": "Training Scheduled",

View File

@ -24,19 +24,19 @@
{% set start = frappe.utils.get_datetime(doc.start_time) %}
{% set end = frappe.utils.get_datetime(doc.end_time) %}
{% if start.date() == end.date() %}
<li>{{_("Date")}}: <b>{{ start.strftime("%A, %d %b %Y") }}</b></li>
<li>
{{_("Timing")}}: <b>{{ start.strftime("%I:%M %p") + ' to ' + end.strftime("%I:%M %p") }}</b>
</li>
<li>{{_("Date")}}: <b>{{ start.strftime("%A, %d %b %Y") }}</b></li>
<li>
{{_("Timing")}}: <b>{{ start.strftime("%I:%M %p") + ' to ' + end.strftime("%I:%M %p") }}</b>
</li>
{% else %}
<li>{{_("Start Time")}}: <b>{{ start.strftime("%A, %d %b %Y at %I:%M %p") }}</b>
</li>
<li>{{_("End Time")}}: <b>{{ end.strftime("%A, %d %b %Y at %I:%M %p") }}</b>
</li>
<li>
{{_("Start Time")}}: <b>{{ start.strftime("%A, %d %b %Y at %I:%M %p") }}</b>
</li>
<li>{{_("End Time")}}: <b>{{ end.strftime("%A, %d %b %Y at %I:%M %p") }}</b></li>
{% endif %}
<li>{{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>
<li>{{ _("Event Link") }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>
{% if doc.is_mandatory %}
<li>Note: This Training Event is mandatory</li>
<li>{{ _("Note: This Training Event is mandatory") }}</li>
{% endif %}
</ul>
</div>
@ -44,4 +44,4 @@
<td width="15"></td>
</tr>
<tr height="10"></tr>
</table>
</table>

View File

@ -178,7 +178,7 @@ def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type):
is_carry_forward, is_expired
FROM `tabLeave Ledger Entry`
WHERE employee=%(employee)s AND leave_type=%(leave_type)s
AND docstatus=1 AND leaves>0
AND docstatus=1
AND (from_date between %(from_date)s AND %(to_date)s
OR to_date between %(from_date)s AND %(to_date)s
OR (from_date < %(from_date)s AND to_date > %(to_date)s))

View File

@ -153,6 +153,24 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Grievance Type",
"link_to": "Grievance Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Grievance",
"link_to": "Employee Grievance",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
@ -823,7 +841,7 @@
"type": "Link"
}
],
"modified": "2021-04-26 13:36:15.413819",
"modified": "2021-05-13 17:19:40.524444",
"modified_by": "Administrator",
"module": "HR",
"name": "HR",

View File

@ -60,8 +60,9 @@ class Loan(AccountsController):
self.monthly_repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods)
def check_sanctioned_amount_limit(self):
total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company)
if sanctioned_amount_limit:
total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit):
frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant)))
@ -155,9 +156,29 @@ def update_total_amount_paid(doc):
frappe.db.set_value("Loan", doc.name, "total_amount_paid", total_amount_paid)
def get_total_loan_amount(applicant_type, applicant, company):
return frappe.db.get_value('Loan',
{'applicant_type': applicant_type, 'company': company, 'applicant': applicant, 'docstatus': 1},
'sum(loan_amount)')
pending_amount = 0
loan_details = frappe.db.get_all("Loan",
filters={"applicant_type": applicant_type, "company": company, "applicant": applicant, "docstatus": 1,
"status": ("!=", "Closed")},
fields=["status", "total_payment", "disbursed_amount", "total_interest_payable", "total_principal_paid",
"written_off_amount"])
interest_amount = flt(frappe.db.get_value("Loan Interest Accrual", {"applicant_type": applicant_type,
"company": company, "applicant": applicant, "docstatus": 1}, "sum(interest_amount - paid_interest_amount)"))
for loan in loan_details:
if loan.status in ("Disbursed", "Loan Closure Requested"):
pending_amount += flt(loan.total_payment) - flt(loan.total_interest_payable) \
- flt(loan.total_principal_paid) - flt(loan.written_off_amount)
elif loan.status == "Partially Disbursed":
pending_amount += flt(loan.disbursed_amount) - flt(loan.total_interest_payable) \
- flt(loan.total_principal_paid) - flt(loan.written_off_amount)
elif loan.status == "Sanctioned":
pending_amount += flt(loan.total_payment)
pending_amount += interest_amount
return pending_amount
def get_sanctioned_amount_limit(applicant_type, applicant, company):
return frappe.db.get_value('Sanctioned Loan Amount',

View File

@ -49,7 +49,11 @@ class TestLoan(unittest.TestCase):
if not frappe.db.exists("Customer", "_Test Loan Customer"):
frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True)
self.applicant2 = frappe.db.get_value("Customer", {'name': '_Test Loan Customer'}, 'name')
if not frappe.db.exists("Customer", "_Test Loan Customer 1"):
frappe.get_doc(get_customer_dict("_Test Loan Customer 1")).insert(ignore_permissions=True)
self.applicant2 = frappe.db.get_value("Customer", {"name": "_Test Loan Customer"}, "name")
self.applicant3 = frappe.db.get_value("Customer", {"name": "_Test Loan Customer 1"}, "name")
create_loan(self.applicant1, "Personal Loan", 280000, "Repay Over Number of Periods", 20)
@ -125,6 +129,38 @@ class TestLoan(unittest.TestCase):
self.assertTrue(gl_entries1)
self.assertTrue(gl_entries2)
def test_sanctioned_amount_limit(self):
# Clear loan docs before checking
frappe.db.sql("DELETE FROM `tabLoan` where applicant = '_Test Loan Customer 1'")
frappe.db.sql("DELETE FROM `tabLoan Application` where applicant = '_Test Loan Customer 1'")
frappe.db.sql("DELETE FROM `tabLoan Security Pledge` where applicant = '_Test Loan Customer 1'")
if not frappe.db.get_value("Sanctioned Loan Amount", filters={"applicant_type": "Customer",
"applicant": "_Test Loan Customer 1", "company": "_Test Company"}):
frappe.get_doc({
"doctype": "Sanctioned Loan Amount",
"applicant_type": "Customer",
"applicant": "_Test Loan Customer 1",
"sanctioned_amount_limit": 1500000,
"company": "_Test Company"
}).insert(ignore_permissions=True)
# Make First Loan
pledge = [{
"loan_security": "Test Security 1",
"qty": 4000.00
}]
loan_application = create_loan_application('_Test Company', self.applicant3, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant3, "Demand Loan", loan_application, posting_date='2019-10-01')
loan.submit()
# Make second loan greater than the sanctioned amount
loan_application = create_loan_application('_Test Company', self.applicant3, 'Demand Loan', pledge,
do_not_save=True)
self.assertRaises(frappe.ValidationError, loan_application.save)
def test_regular_loan_repayment(self):
pledge = [{
"loan_security": "Test Security 1",
@ -367,7 +403,7 @@ class TestLoan(unittest.TestCase):
unpledge_request.load_from_db()
self.assertEqual(unpledge_request.docstatus, 1)
def test_santined_loan_security_unpledge(self):
def test_sanctioned_loan_security_unpledge(self):
pledge = [{
"loan_security": "Test Security 1",
"qty": 4000.00
@ -858,7 +894,7 @@ def create_repayment_entry(loan, applicant, posting_date, paid_amount):
return lr
def create_loan_application(company, applicant, loan_type, proposed_pledges, repayment_method=None,
repayment_periods=None, posting_date=None):
repayment_periods=None, posting_date=None, do_not_save=False):
loan_application = frappe.new_doc('Loan Application')
loan_application.applicant_type = 'Customer'
loan_application.company = company
@ -874,6 +910,9 @@ def create_loan_application(company, applicant, loan_type, proposed_pledges, rep
for pledge in proposed_pledges:
loan_application.append('proposed_pledges', pledge)
if do_not_save:
return loan_application
loan_application.save()
loan_application.submit()

View File

@ -46,9 +46,11 @@ class LoanApplication(Document):
frappe.throw(_("Loan Amount exceeds maximum loan amount of {0} as per proposed securities").format(self.maximum_loan_amount))
def check_sanctioned_amount_limit(self):
total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company)
if sanctioned_amount_limit:
total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit):
frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant)))

View File

@ -235,70 +235,71 @@ class LoanRepayment(AccountsController):
else:
remarks = _("Repayment against Loan: ") + self.against_loan
if self.total_penalty_paid:
if not loan_details.repay_from_salary:
if self.total_penalty_paid:
gle_map.append(
self.get_gl_dict({
"account": loan_details.loan_account,
"against": loan_details.payment_account,
"debit": self.total_penalty_paid,
"debit_in_account_currency": self.total_penalty_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": _("Penalty against loan:") + self.against_loan,
"cost_center": self.cost_center,
"party_type": self.applicant_type,
"party": self.applicant,
"posting_date": getdate(self.posting_date)
})
)
gle_map.append(
self.get_gl_dict({
"account": loan_details.penalty_income_account,
"against": loan_details.payment_account,
"credit": self.total_penalty_paid,
"credit_in_account_currency": self.total_penalty_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": _("Penalty against loan:") + self.against_loan,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date)
})
)
gle_map.append(
self.get_gl_dict({
"account": loan_details.loan_account,
"against": loan_details.payment_account,
"debit": self.total_penalty_paid,
"debit_in_account_currency": self.total_penalty_paid,
"account": loan_details.payment_account,
"against": loan_details.loan_account + ", " + loan_details.interest_income_account
+ ", " + loan_details.penalty_income_account,
"debit": self.amount_paid,
"debit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": _("Penalty against loan:") + self.against_loan,
"remarks": remarks,
"cost_center": self.cost_center,
"party_type": self.applicant_type,
"party": self.applicant,
"posting_date": getdate(self.posting_date)
})
)
gle_map.append(
self.get_gl_dict({
"account": loan_details.penalty_income_account,
"account": loan_details.loan_account,
"party_type": loan_details.applicant_type,
"party": loan_details.applicant,
"against": loan_details.payment_account,
"credit": self.total_penalty_paid,
"credit_in_account_currency": self.total_penalty_paid,
"credit": self.amount_paid,
"credit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": _("Penalty against loan:") + self.against_loan,
"remarks": remarks,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date)
})
)
gle_map.append(
self.get_gl_dict({
"account": loan_details.payment_account,
"against": loan_details.loan_account + ", " + loan_details.interest_income_account
+ ", " + loan_details.penalty_income_account,
"debit": self.amount_paid,
"debit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": remarks,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date)
})
)
gle_map.append(
self.get_gl_dict({
"account": loan_details.loan_account,
"party_type": loan_details.applicant_type,
"party": loan_details.applicant,
"against": loan_details.payment_account,
"credit": self.amount_paid,
"credit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": remarks,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date)
})
)
if gle_map:
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False)
if gle_map:
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False)
def create_repayment_entry(loan, applicant, company, posting_date, loan_type,
payment_type, interest_payable, payable_principal_amount, amount_paid, penalty_amount=None):

View File

@ -71,7 +71,6 @@ frappe.ui.form.on("BOM", {
refresh: function(frm) {
frm.toggle_enable("item", frm.doc.__islocal);
toggle_operations(frm);
frm.set_indicator_formatter('item_code',
function(doc) {
@ -326,8 +325,7 @@ frappe.ui.form.on("BOM", {
freeze: true,
args: {
update_parent: true,
from_child_bom:false,
save: frm.doc.docstatus === 1 ? true : false
from_child_bom:false
},
callback: function(r) {
refresh_field("items");
@ -651,15 +649,8 @@ frappe.ui.form.on("BOM Item", "items_remove", function(frm) {
erpnext.bom.calculate_total(frm.doc);
});
var toggle_operations = function(frm) {
frm.toggle_display("operations_section", cint(frm.doc.with_operations) == 1);
frm.toggle_display("transfer_material_against", cint(frm.doc.with_operations) == 1);
frm.toggle_reqd("transfer_material_against", cint(frm.doc.with_operations) == 1);
};
frappe.ui.form.on("BOM", "with_operations", function(frm) {
if(!cint(frm.doc.with_operations)) {
frm.set_value("operations", []);
}
toggle_operations(frm);
});

View File

@ -193,6 +193,7 @@
},
{
"default": "Work Order",
"depends_on": "with_operations",
"fieldname": "transfer_material_against",
"fieldtype": "Select",
"label": "Transfer Material Against",
@ -235,6 +236,7 @@
{
"fieldname": "operations_section",
"fieldtype": "Section Break",
"hide_border": 1,
"oldfieldtype": "Section Break"
},
{
@ -245,6 +247,7 @@
"options": "Routing"
},
{
"depends_on": "with_operations",
"fieldname": "operations",
"fieldtype": "Table",
"label": "Operations",
@ -517,7 +520,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
"modified": "2020-05-21 12:29:32.634952",
"modified": "2021-03-16 12:25:09.081968",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",

Some files were not shown because too many files have changed in this diff Show More