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 erpnext.hooks import regional_overrides
from frappe.utils import getdate from frappe.utils import getdate
__version__ = '13.5.2' __version__ = '13.6.0'
def get_default_company(user=None): def get_default_company(user=None):
'''Get default company for user''' '''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', if address and frappe.db.get_value('Dynamic Link',
{'parent': address, 'link_name': company}): {'parent': address, 'link_name': company}):
filters.append(["Address", "name", "=", address]) 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 {} address = frappe.get_all("Address", filters=filters, fields=fields) or {}

View File

@ -19,7 +19,7 @@ class AccountingDimension(Document):
def validate(self): def validate(self):
if self.document_type in core_doctypes_list + ('Accounting Dimension', 'Project', 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) msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
frappe.throw(msg) frappe.throw(msg)

View File

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

View File

@ -54,7 +54,7 @@ class CForm(Document):
frappe.throw(_("Please enter atleast 1 invoice in the table")) frappe.throw(_("Please enter atleast 1 invoice in the table"))
def set_total_invoiced_amount(self): 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.db.set(self, 'total_invoiced_amount', total)
@frappe.whitelist() @frappe.whitelist()

View File

@ -14,7 +14,7 @@ class CouponCode(Document):
if not self.coupon_code: if not self.coupon_code:
if self.coupon_type == "Promotional": 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": elif self.coupon_type == "Gift Card":
self.coupon_code = frappe.generate_hash()[:10].upper() self.coupon_code = frappe.generate_hash()[:10].upper()

View File

@ -86,7 +86,7 @@ def resolve_dunning(doc, state):
for reference in doc.references: for reference in doc.references:
if reference.reference_doctype == 'Sales Invoice' and reference.outstanding_amount <= 0: if reference.reference_doctype == 'Sales Invoice' and reference.outstanding_amount <= 0:
dunnings = frappe.get_list('Dunning', filters={ 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: for dunning in dunnings:
frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved') frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved')

View File

@ -121,8 +121,7 @@ class GLEntry(Document):
def check_pl_account(self): def check_pl_account(self):
if self.is_opening=='Yes' and \ if self.is_opening=='Yes' and \
frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss" and \ frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss":
self.voucher_type not in ['Purchase Invoice', 'Sales Invoice']:
frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry") frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry")
.format(self.voucher_type, self.voucher_no, self.account)) .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))) record.idx, frappe.bold(actual_outstanding), frappe.bold(record.sales_invoice)))
def calculate_total_amount(self): 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): def on_submit(self):
self.update_sales_invoice() 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)) 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): def check_credit_limit(self):
customers = list(set([d.party for d in self.get("accounts") 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 d.party_type=="Customer" and d.party and flt(d.debit) > 0))
if customers: if customers:
from erpnext.selling.doctype.customer.customer import check_credit_limit from erpnext.selling.doctype.customer.customer import check_credit_limit
for customer in customers: for customer in customers:

View File

@ -21,7 +21,7 @@ class MonthlyDistribution(Document):
idx += 1 idx += 1
def validate(self): 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: if flt(total, 2) != 100.0:
frappe.throw(_("Percentage Allocation should be equal to 100%") + \ frappe.throw(_("Percentage Allocation should be equal to 100%") + \

View File

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

View File

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

View File

@ -112,7 +112,7 @@ class PaymentRequest(Document):
if not data_of_completed_requests: if not data_of_completed_requests:
return self.grand_total 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 return request_amounts
def on_cancel(self): def on_cancel(self):

View File

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

View File

@ -183,7 +183,7 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
condition = "ifnull({table}.{field}, '') in ({parent_groups})".format( condition = "ifnull({table}.{field}, '') in ({parent_groups})".format(
table=table, table=table,
field=field, 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 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 # find pricing rule with highest priority
if pricing_rules: 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: if max_priority:
pricing_rules = list(filter(lambda x: cint(x.priority)==max_priority, pricing_rules)) 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) pricing_rules = list(pricing_rules)
if len(pricing_rules) > 1: 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": 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)) \ pricing_rules = list(filter(lambda x: x.for_price_list==args.price_list, pricing_rules)) \
or pricing_rules or pricing_rules
if len(pricing_rules) > 1 and not args.for_shopping_cart: 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}") 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: elif pricing_rules:
return pricing_rules[0] 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): def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values=False):
if pricing_rule_args: 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: for args in pricing_rule_args:
if not items or (args.get('item_code'), args.get('pricing_rules')) not in items: if not items or (args.get('item_code'), args.get('pricing_rules')) not in items:

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() { onload: function() {
this._super(); this._super();
@ -569,5 +565,9 @@ frappe.ui.form.on("Purchase Invoice", {
frm: frm, frm: frm,
freeze_message: __("Creating Purchase Receipt ...") 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 # because updating ordered qty in bin depends upon updated ordered qty in PO
if self.update_stock == 1: if self.update_stock == 1:
self.update_stock_ledger() self.update_stock_ledger()
self.set_consumed_qty_in_po()
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
update_serial_nos_after_submit(self, "items") update_serial_nos_after_submit(self, "items")
@ -998,6 +999,7 @@ class PurchaseInvoice(BuyingController):
if self.update_stock == 1: if self.update_stock == 1:
self.update_stock_ledger() self.update_stock_ledger()
self.delete_auto_created_batches() self.delete_auto_created_batches()
self.set_consumed_qty_in_po()
self.make_gl_entries_on_cancel() 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()) self.assertEqual(actual_qty_0, get_qty_after_transaction())
def test_subcontracting_via_purchase_invoice(self): 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 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", 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", make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse 1 - _TC",
qty=100, basic_rate=100) qty=100, basic_rate=100)
@ -632,7 +634,7 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(len(pi.get("supplied_items")), 2) 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)) self.assertEqual(flt(pi.get("items")[0].rm_supp_cost, 2), flt(rm_supp_cost, 2))
def test_rejected_serial_no(self): def test_rejected_serial_no(self):
@ -964,7 +966,7 @@ class TestPurchaseInvoice(unittest.TestCase):
update_tax_witholding_category('_Test Company', 'TDS Payable - _TC', nowdate()) update_tax_witholding_category('_Test Company', 'TDS Payable - _TC', nowdate())
# Create Purchase Order with TDS applied # 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.apply_tds = 1
po.tax_withholding_category = 'TDS - 194 - Dividends - Individual' po.tax_withholding_category = 'TDS - 194 - Dividends - Individual'
po.save() po.save()
@ -1000,6 +1002,7 @@ class TestPurchaseInvoice(unittest.TestCase):
# Create Purchase Invoice against Purchase Order # Create Purchase Invoice against Purchase Order
purchase_invoice = get_mapped_purchase_invoice(po.name) purchase_invoice = get_mapped_purchase_invoice(po.name)
purchase_invoice.allocate_advances_automatically = 1 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.items[0].expense_account = '_Test Account Cost for Goods Sold - _TC'
purchase_invoice.save() purchase_invoice.save()
purchase_invoice.submit() purchase_invoice.submit()

View File

@ -272,7 +272,7 @@
"fieldname": "rate", "fieldname": "rate",
"fieldtype": "Currency", "fieldtype": "Currency",
"in_list_view": 1, "in_list_view": 1,
"label": "Rate ", "label": "Rate",
"oldfieldname": "import_rate", "oldfieldname": "import_rate",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"options": "currency", "options": "currency",
@ -854,7 +854,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-03-30 09:02:39.256602", "modified": "2021-06-16 19:57:03.101571",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "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 = create_sales_invoice(customer = "Test TCS Customer", rate=5000)
si.submit() 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) self.assertEqual(tcs_charged, 500)
invoices.append(si) 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): def check_if_in_list(gle, gl_map, dimensions=None):
account_head_fieldnames = ['party_type', 'party', 'against_voucher', 'against_voucher_type', account_head_fieldnames = ['party_type', 'party', 'against_voucher', 'against_voucher_type',
'cost_center', 'project'] 'cost_center', 'project', 'voucher_detail_no']
if dimensions: if dimensions:
account_head_fieldnames = account_head_fieldnames + 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) validate_expense_against_budget(args)
def validate_cwip_accounts(gl_map): 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": 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 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 join `tabCash Flow Mapping` cfm on cfma.parent=cfm.name
where cfma.parent in (%s) where cfma.parent in (%s)
order by cfm.is_working_capital 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): def setup_mappers(mappers):
@ -83,8 +83,8 @@ def setup_mappers(mappers):
account_types_labels = sorted( account_types_labels = sorted(
set( set(
[(d['label'], d['is_working_capital'], d['is_income_tax_liability'], d['is_income_tax_expense']) (d['label'], d['is_working_capital'], d['is_income_tax_liability'], d['is_income_tax_expense'])
for d in account_types] for d in account_types
), ),
key=lambda x: x[1] key=lambda x: x[1]
) )
@ -375,7 +375,7 @@ def _get_account_type_based_data(filters, account_names, period_list, accumulate
total = 0 total = 0
for period in period_list: for period in period_list:
start_date = get_start_date(period, accumulated_values, company) 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: if opening_balances:
date_info = dict(date=start_date) date_info = dict(date=start_date)

View File

@ -145,7 +145,7 @@ class PartyLedgerSummaryReport(object):
out = [] out = []
for party, row in iteritems(self.party_data): 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: 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 row.paid_amount -= total_party_adjustment
adjustments = self.party_adjustment_details.get(party, {}) adjustments = self.party_adjustment_details.get(party, {})

View File

@ -369,7 +369,7 @@ def set_gl_entries_by_account(
if accounts: if accounts:
additional_conditions += " and account in ({})"\ 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 = { gl_filters = {
"company": company, "company": company,

View File

@ -36,16 +36,12 @@ frappe.query_reports["General Ledger"] = {
{ {
"fieldname":"account", "fieldname":"account",
"label": __("Account"), "label": __("Account"),
"fieldtype": "Link", "fieldtype": "MultiSelectList",
"options": "Account", "options": "Account",
"get_query": function() { get_data: function(txt) {
var company = frappe.query_report.get_filter_value('company'); return frappe.db.get_link_options('Account', txt, {
return { company: frappe.query_report.get_filter_value("company")
"doctype": "Account", });
"filters": {
"company": company,
}
}
} }
}, },
{ {
@ -135,7 +131,9 @@ frappe.query_reports["General Ledger"] = {
"label": __("Cost Center"), "label": __("Cost Center"),
"fieldtype": "MultiSelectList", "fieldtype": "MultiSelectList",
get_data: function(txt) { 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"), "label": __("Project"),
"fieldtype": "MultiSelectList", "fieldtype": "MultiSelectList",
get_data: function(txt) { 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"): 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")))) 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): for account in filters.account:
frappe.throw(_("Account {0} does not exists").format(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') if (filters.get("account") and filters.get("group_by") == _('Group by Account')
and account_details[filters.account].is_group == 0): and account_details[filters.account].is_group == 0):
@ -87,7 +91,19 @@ def set_account_currency(filters):
account_currency = None account_currency = None
if filters.get("account"): 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"): elif filters.get("party"):
gle_currency = frappe.db.get_value( gle_currency = frappe.db.get_value(
"GL Entry", { "GL Entry", {
@ -205,10 +221,10 @@ def get_gl_entries(filters, accounting_dimensions):
def get_conditions(filters): def get_conditions(filters):
conditions = [] conditions = []
if filters.get("account") and not filters.get("include_dimensions"):
lft, rgt = frappe.db.get_value("Account", filters["account"], ["lft", "rgt"]) if filters.get("account"):
conditions.append("""account in (select name from tabAccount filters.account = get_accounts_with_children(filters.account)
where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt)) conditions.append("account in %(account)s")
if filters.get("cost_center"): if filters.get("cost_center"):
filters.cost_center = get_cost_centers_with_children(filters.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 "" 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): def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries):
data = [] data = []

View File

@ -334,7 +334,7 @@ def get_aii_accounts():
def get_purchase_receipts_against_purchase_order(item_list): def get_purchase_receipts_against_purchase_order(item_list):
po_pr_map = frappe._dict() 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: if po_item_rows:
purchase_receipts = frappe.db.sql(""" 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: if item_list:
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency) 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) so_dn_map = get_delivery_notes_against_sales_order(item_list)
data = [] data = []

View File

@ -77,14 +77,14 @@ def get_pos_entries(filters, group_by_field):
), filters, as_dict=1) ), filters, as_dict=1)
def concat_mode_of_payments(pos_entries): 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: for entry in pos_entries:
if mode_of_payments.get(entry.pos_invoice): if mode_of_payments.get(entry.pos_invoice):
entry.mode_of_payment = ", ".join(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): def add_subtotal_row(data, group_invoices, group_by_field, group_by_value):
grand_total = sum([d.grand_total 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]) paid_amount = sum(d.paid_amount for d in group_invoices)
data.append({ data.append({
group_by_field: group_by_value, group_by_field: group_by_value,
"grand_total": grand_total, "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, invoice_tax_map = get_invoice_tax_map(invoice_list,
invoice_expense_map, expense_accounts) invoice_expense_map, expense_accounts)
invoice_po_pr_map = get_invoice_po_pr_map(invoice_list) 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) supplier_details = get_supplier_details(suppliers)
company_currency = frappe.get_cached_value('Company', filters.company, "default_currency") 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 docstatus = 1 and (account_head is not null and account_head != '')
and category in ('Total', 'Valuation and Total') and category in ('Total', 'Valuation and Total')
and parent in (%s) order by account_head""" % 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 unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account
from `tabPurchase Invoice` where docstatus = 1 and name in (%s) from `tabPurchase Invoice` where docstatus = 1 and name in (%s)
and ifnull(unrealized_profit_loss_account, '') != '' and ifnull(unrealized_profit_loss_account, '') != ''
order by 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] 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] 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` from `tabPurchase Invoice Item`
where parent in (%s) where parent in (%s)
group by parent, expense_account 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 = {} invoice_expense_map = {}
for d in expense_details: 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, unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account,
base_net_total as amount from `tabPurchase Invoice` where name in (%s) base_net_total as amount from `tabPurchase Invoice` where name in (%s)
and is_internal_supplier = 1 and company = represents_company""" % 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 = {} internal_invoice_map = {}
for d in unrealized_amount_details: 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') where parent in (%s) and category in ('Total', 'Valuation and Total')
and base_tax_amount_after_discount_amount != 0 and base_tax_amount_after_discount_amount != 0
group by parent, account_head, add_deduct_tax 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 = {} invoice_tax_map = {}
for d in tax_details: 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 select parent, purchase_order, purchase_receipt, po_detail, project
from `tabPurchase Invoice Item` from `tabPurchase Invoice Item`
where parent in (%s) 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 = {} invoice_po_pr_map = {}
for d in pi_items: for d in pi_items:

View File

@ -158,7 +158,7 @@ def get_sales_invoice_data(filters):
def get_mode_of_payments(filters): def get_mode_of_payments(filters):
mode_of_payments = {} mode_of_payments = {}
invoice_list = get_invoices(filters) 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: if invoice_list:
inv_mop = frappe.db.sql("""select a.owner,a.posting_date, ifnull(b.mode_of_payment, '') as mode_of_payment 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 from `tabSales Invoice` a, `tabSales Invoice Payment` b
@ -197,7 +197,7 @@ def get_invoices(filters):
def get_mode_of_payment_details(filters): def get_mode_of_payment_details(filters):
mode_of_payment_details = {} mode_of_payment_details = {}
invoice_list = get_invoices(filters) 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: if invoice_list:
inv_mop_detail = frappe.db.sql("""select a.owner, a.posting_date, 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 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 income_accounts = frappe.db.sql_list("""select distinct income_account
from `tabSales Invoice Item` where docstatus = 1 and parent in (%s) from `tabSales Invoice Item` where docstatus = 1 and parent in (%s)
order by income_account""" % 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 tax_accounts = frappe.db.sql_list("""select distinct account_head
from `tabSales Taxes and Charges` where parenttype = 'Sales Invoice' from `tabSales Taxes and Charges` where parenttype = 'Sales Invoice'
and docstatus = 1 and base_tax_amount_after_discount_amount != 0 and docstatus = 1 and base_tax_amount_after_discount_amount != 0
and parent in (%s) order by account_head""" % 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 unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account
from `tabSales Invoice` where docstatus = 1 and name in (%s) from `tabSales Invoice` where docstatus = 1 and name in (%s)
and ifnull(unrealized_profit_loss_account, '') != '' and ifnull(unrealized_profit_loss_account, '') != ''
order by 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: for account in income_accounts:
income_columns.append({ income_columns.append({
@ -406,7 +406,7 @@ def get_invoices(filters, additional_query_columns):
def get_invoice_income_map(invoice_list): def get_invoice_income_map(invoice_list):
income_details = frappe.db.sql("""select parent, income_account, sum(base_net_amount) as amount 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""" % 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 = {} invoice_income_map = {}
for d in income_details: 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, unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account,
base_net_total as amount from `tabSales Invoice` where name in (%s) base_net_total as amount from `tabSales Invoice` where name in (%s)
and is_internal_customer = 1 and company = represents_company""" % 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 = {} internal_invoice_map = {}
for d in unrealized_amount_details: 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, tax_details = frappe.db.sql("""select parent, account_head,
sum(base_tax_amount_after_discount_amount) as tax_amount sum(base_tax_amount_after_discount_amount) as tax_amount
from `tabSales Taxes and Charges` where parent in (%s) group by parent, account_head""" % 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 = {} invoice_tax_map = {}
for d in tax_details: 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 si_items = frappe.db.sql("""select parent, sales_order, delivery_note, so_detail
from `tabSales Invoice Item` where parent in (%s) from `tabSales Invoice Item` where parent in (%s)
and (ifnull(sales_order, '') != '' or ifnull(delivery_note, '') != '')""" % 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 = {} invoice_so_dn_map = {}
for d in si_items: 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 si_items = frappe.db.sql("""select parent, cost_center, warehouse
from `tabSales Invoice Item` where parent in (%s) from `tabSales Invoice Item` where parent in (%s)
and (ifnull(cost_center, '') != '' or ifnull(warehouse, '') != '')""" % 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 = {} invoice_cc_wh_map = {}
for d in si_items: 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 and company=%s and posting_date between %s and %s
""", (supplier, company, from_date, to_date), as_dict=1) """, (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 = [d.voucher_no for d in entries]
vouchers += get_advance_vouchers([supplier], company=company, 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` from `tabGL Entry`
where account=%s and posting_date between %s and %s where account=%s and posting_date between %s and %s
and company=%s and credit > 0 and voucher_no in ({0}) 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]) (account, from_date, to_date, company))[0][0])
date_range_filter = [fiscal_year, from_date, to_date] 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) gross_profit_data = GrossProfitGenerator(filters)
result = gross_profit_data.grouped_data result = gross_profit_data.grouped_data
if not with_item_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 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()', 'select name from `tabPurchase Invoice` where release_date IS NOT NULL and release_date > CURDATE()',
as_dict=1 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 return held_invoices

View File

@ -470,7 +470,7 @@ class TestAsset(unittest.TestCase):
}) })
asset.insert() asset.insert()
accumulated_depreciation_after_full_schedule = \ 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) - asset_value_after_full_schedule = (flt(asset.gross_purchase_amount) -
flt(accumulated_depreciation_after_full_schedule)) flt(accumulated_depreciation_after_full_schedule))

View File

@ -92,7 +92,7 @@ class AssetValueAdjustment(Document):
d.value_after_depreciation = asset_value d.value_after_depreciation = asset_value
if d.depreciation_method in ("Straight Line", "Manual"): 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) total_days = date_diff(end_date, self.date)
rate_per_day = flt(d.value_after_depreciation) / flt(total_days) rate_per_day = flt(d.value_after_depreciation) / flt(total_days)
from_date = self.date from_date = self.date

View File

@ -9,13 +9,14 @@
"supp_master_name", "supp_master_name",
"supplier_group", "supplier_group",
"buying_price_list", "buying_price_list",
"maintain_same_rate_action",
"role_to_override_stop_action",
"column_break_3", "column_break_3",
"po_required", "po_required",
"pr_required", "pr_required",
"maintain_same_rate", "maintain_same_rate",
"maintain_same_rate_action",
"role_to_override_stop_action",
"allow_multiple_items", "allow_multiple_items",
"bill_for_rejected_quantity_in_purchase_invoice",
"subcontract", "subcontract",
"backflush_raw_materials_of_subcontract_based_on", "backflush_raw_materials_of_subcontract_based_on",
"column_break_11", "column_break_11",
@ -108,6 +109,13 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Role Allowed to Override Stop Action", "label": "Role Allowed to Override Stop Action",
"options": "Role" "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", "icon": "fa fa-cog",
@ -115,7 +123,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-04-04 20:01:44.087066", "modified": "2021-06-24 10:38:28.934525",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying Settings", "name": "Buying Settings",

View File

@ -53,6 +53,39 @@ frappe.ui.form.on("Purchase Order", {
} else { } else {
frm.set_value("tax_withholding_category", frm.supplier_tds); 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() { 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() { make_stock_entry: function() {
@ -513,12 +546,14 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
], ],
primary_action: function() { primary_action: function() {
var data = d.get_values(); var data = d.get_values();
let reason_for_hold = 'Reason for hold: ' + data.reason_for_hold;
frappe.call({ frappe.call({
method: "frappe.desk.form.utils.add_comment", method: "frappe.desk.form.utils.add_comment",
args: { args: {
reference_doctype: me.frm.doctype, reference_doctype: me.frm.doctype,
reference_name: me.frm.docname, reference_name: me.frm.docname,
content: __('Reason for hold:') + " " +data.reason_for_hold, content: __(reason_for_hold),
comment_email: frappe.session.user, comment_email: frappe.session.user,
comment_by: frappe.session.user_fullname comment_by: frappe.session.user_fullname
}, },

View File

@ -609,6 +609,7 @@
"fieldname": "supplied_items", "fieldname": "supplied_items",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Supplied Items", "label": "Supplied Items",
"no_copy": 1,
"oldfieldname": "po_raw_material_details", "oldfieldname": "po_raw_material_details",
"oldfieldtype": "Table", "oldfieldtype": "Table",
"options": "Purchase Order Item Supplied", "options": "Purchase Order Item Supplied",
@ -1377,7 +1378,7 @@
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-04-19 00:55:30.781375", "modified": "2021-05-30 15:17:53.663648",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order", "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.buying.utils import validate_for_items, check_on_hold_or_closed_status
from erpnext.stock.utils import get_bin from erpnext.stock.utils import get_bin
from erpnext.accounts.party import get_party_account_currency 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.stock.doctype.item.item import get_item_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_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.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,\ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (validate_inter_company_party,
unlink_inter_company_doc update_linked_doc, unlink_inter_company_doc)
form_grid_templates = { form_grid_templates = {
"items": "templates/form_grid/item_grid.html" "items": "templates/form_grid/item_grid.html"
@ -139,7 +138,7 @@ class PurchaseOrder(BuyingController):
def validate_minimum_order_qty(self): def validate_minimum_order_qty(self):
if not self.get("items"): return 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 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)) from tabItem where name in ({0})""".format(", ".join(["%s"] * len(items))), items))
@ -326,10 +325,10 @@ class PurchaseOrder(BuyingController):
so.notify_update() so.notify_update()
def has_drop_ship_item(self): 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): 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): def set_received_qty_for_drop_ship_items(self):
for item in self.items: for item in self.items:
@ -503,9 +502,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
@frappe.whitelist() @frappe.whitelist()
def make_rm_stock_entry(purchase_order, rm_items): 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) rm_items_list = json.loads(rm_items)
else: elif not rm_items:
frappe.throw(_("No Items available for transfer")) frappe.throw(_("No Items available for transfer"))
if rm_items_list: if rm_items_list:
@ -543,6 +544,8 @@ def make_rm_stock_entry(purchase_order, rm_items):
'qty': rm_item_data["qty"], 'qty': rm_item_data["qty"],
'from_warehouse': rm_item_data["warehouse"], 'from_warehouse': rm_item_data["warehouse"],
'stock_uom': rm_item_data["stock_uom"], '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"], 'main_item_code': rm_item_data["item_code"],
'allow_alternative_item': item_wh.get(rm_item_code, {}).get('allow_alternative_item') '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): def make_inter_company_sales_order(source_name, target_doc=None):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction
return make_inter_company_transaction("Purchase Order", source_name, target_doc) 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.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order
from erpnext.stock.doctype.batch.test_batch import make_new_batch 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): class TestPurchaseOrder(unittest.TestCase):
def test_make_purchase_receipt(self): def test_make_purchase_receipt(self):
@ -359,7 +358,7 @@ class TestPurchaseOrder(unittest.TestCase):
update_child_qty_rate('Purchase Order', trans_item, po.name) update_child_qty_rate('Purchase Order', trans_item, po.name)
po.reload() 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) 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) self.assertEqual(bin11.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
def test_exploded_items_in_subcontracted(self): 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) make_subcontracted_item(item_code=item_code)
po = create_purchase_order(item_code=item_code, qty=1, po = create_purchase_order(item_code=item_code, qty=1,
@ -848,79 +847,6 @@ class TestPurchaseOrder(unittest.TestCase):
for item in rm_items: for item in rm_items:
transferred_rm_map[item.get('rm_item_code')] = item 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") update_backflush_based_on("BOM")
def test_supplied_qty_against_subcontracted_po(self): def test_supplied_qty_against_subcontracted_po(self):
@ -1117,6 +1043,10 @@ def create_purchase_order(**args):
po.conversion_factor = args.conversion_factor or 1 po.conversion_factor = args.conversion_factor or 1
po.supplier_warehouse = args.supplier_warehouse or None po.supplier_warehouse = args.supplier_warehouse or None
if args.rm_items:
for row in args.rm_items:
po.append("items", row)
else:
po.append("items", { po.append("items", {
"item_code": args.item or args.item_code or "_Test Item", "item_code": args.item or args.item_code or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC", "warehouse": args.warehouse or "_Test Warehouse - _TC",
@ -1126,12 +1056,15 @@ def create_purchase_order(**args):
"include_exploded_items": args.get('include_exploded_items', 1), "include_exploded_items": args.get('include_exploded_items', 1),
"against_blanket_order": args.against_blanket_order "against_blanket_order": args.against_blanket_order
}) })
po.set_missing_values()
if not args.do_not_save: if not args.do_not_save:
po.insert() po.insert()
if not args.do_not_submit: if not args.do_not_submit:
if po.is_subcontracted == "Yes": if po.is_subcontracted == "Yes":
supp_items = po.get("supplied_items") supp_items = po.get("supplied_items")
for d in supp_items: for d in supp_items:
if not d.reserve_warehouse:
d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC" d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC"
po.submit() po.submit()

View File

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

View File

@ -6,10 +6,11 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"main_item_code", "main_item_code",
"description", "rm_item_code",
"item_name",
"bom_detail_no", "bom_detail_no",
"col_break1", "col_break1",
"rm_item_code", "description",
"stock_uom", "stock_uom",
"conversion_factor", "conversion_factor",
"reference_name", "reference_name",
@ -25,7 +26,8 @@
"secbreak_3", "secbreak_3",
"batch_no", "batch_no",
"col_break4", "col_break4",
"serial_no" "serial_no",
"purchase_order"
], ],
"fields": [ "fields": [
{ {
@ -52,7 +54,6 @@
"fieldname": "description", "fieldname": "description",
"fieldtype": "Text Editor", "fieldtype": "Text Editor",
"in_global_search": 1, "in_global_search": 1,
"in_list_view": 1,
"label": "Description", "label": "Description",
"oldfieldname": "description", "oldfieldname": "description",
"oldfieldtype": "Data", "oldfieldtype": "Data",
@ -81,18 +82,20 @@
"fieldname": "required_qty", "fieldname": "required_qty",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Required Qty", "label": "Available Qty For Consumption",
"oldfieldname": "required_qty", "oldfieldname": "required_qty",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{ {
"columns": 2,
"fieldname": "consumed_qty", "fieldname": "consumed_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Consumed Qty", "in_list_view": 1,
"label": "Qty to Be Consumed",
"oldfieldname": "consumed_qty", "oldfieldname": "consumed_qty",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"read_only": 1,
"reqd": 1 "reqd": 1
}, },
{ {
@ -183,12 +186,28 @@
{ {
"fieldname": "col_break4", "fieldname": "col_break4",
"fieldtype": "Column Break" "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, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-09-18 17:26:09.703215", "modified": "2021-06-19 19:33:04.431213",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Receipt Item Supplied", "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) create_rfq_items(sq_doc, supplier, data)
def create_rfq_items(sq_doc, supplier, data): def create_rfq_items(sq_doc, supplier, data):
sq_doc.append('items', { args = {}
"item_code": data.item_code,
"item_name": data.item_name, for field in ['item_code', 'item_name', 'description', 'qty', 'rate', 'conversion_factor',
"description": data.description, 'warehouse', 'material_request', 'material_request_item', 'stock_qty']:
"qty": data.qty, args[field] = data.get(field)
"rate": data.rate,
"conversion_factor": data.conversion_factor if data.conversion_factor else None, args.update({
"supplier_part_no": frappe.db.get_value("Item Supplier", {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no"),
"warehouse": data.warehouse or '',
"request_for_quotation_item": data.name, "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() @frappe.whitelist()
def get_pdf(doctype, name, supplier): def get_pdf(doctype, name, supplier):
doc = get_rfq_doc(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(): def get_supplier_tag():
if not frappe.cache().hget("Supplier", "Tags"): if not frappe.cache().hget("Supplier", "Tags"):
filters = {"document_type": "Supplier"} 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) frappe.cache().hset("Supplier", "Tags", tags)
return frappe.cache().hget("Supplier", "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: if filters.from_date >= filters.to_date:
frappe.msgprint(_("To Date must be greater than From Date")) frappe.msgprint(_("To Date must be greater than From Date"))
data = []
columns = get_columns() columns = get_columns()
get_data(data , filters) data = get_data(filters)
return columns, data
return columns, data or []
def get_columns(): def get_columns():
return [ return [
@ -21,13 +21,12 @@ def get_columns():
"fieldtype": "Link", "fieldtype": "Link",
"fieldname": "purchase_order", "fieldname": "purchase_order",
"options": "Purchase Order", "options": "Purchase Order",
"width": 150 "width": 200
}, },
{ {
"label": _("Date"), "label": _("Date"),
"fieldtype": "Date", "fieldtype": "Date",
"fieldname": "date", "fieldname": "date",
"hidden": 1,
"width": 150 "width": 150
}, },
{ {
@ -41,97 +40,58 @@ def get_columns():
"label": _("Item Code"), "label": _("Item Code"),
"fieldtype": "Data", "fieldtype": "Data",
"fieldname": "rm_item_code", "fieldname": "rm_item_code",
"width": 100 "width": 150
}, },
{ {
"label": _("Required Quantity"), "label": _("Required Quantity"),
"fieldtype": "Float", "fieldtype": "Float",
"fieldname": "r_qty", "fieldname": "reqd_qty",
"width": 100 "width": 150
}, },
{ {
"label": _("Transferred Quantity"), "label": _("Transferred Quantity"),
"fieldtype": "Float", "fieldtype": "Float",
"fieldname": "t_qty", "fieldname": "transferred_qty",
"width": 100 "width": 200
}, },
{ {
"label": _("Pending Quantity"), "label": _("Pending Quantity"),
"fieldtype": "Float", "fieldtype": "Float",
"fieldname": "p_qty", "fieldname": "p_qty",
"width": 100 "width": 150
} }
] ]
def get_data(data, filters): def get_data(filters):
po = get_po(filters) po_rm_item_details = get_po_items_to_supply(filters)
po_transferred_qty_map = frappe._dict(get_transferred_quantity([v.name for v in po]))
sub_items = get_purchase_order_item_supplied([v.name for v in po])
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
}
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) data.append(row)
return(data) return data
def get_po(filters): def get_po_items_to_supply(filters):
record_filters = [ return frappe.db.get_all(
["is_subcontracted", "=", "Yes"], "Purchase Order",
["supplier", "=", filters.supplier], fields=[
["transaction_date", "<=", filters.to_date], "name as purchase_order",
["transaction_date", ">=", filters.from_date], "transaction_date as date",
["docstatus", "=", 1] "supplier as supplier",
] "`tabPurchase Order Item Supplied`.rm_item_code as rm_item_code",
return frappe.get_all("Purchase Order", filters=record_filters, fields=["name", "transaction_date", "supplier"]) "`tabPurchase Order Item Supplied`.required_qty as reqd_qty",
"`tabPurchase Order Item Supplied`.supplied_qty as transferred_qty"
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]
], ],
fields=["parent", "item_code", "qty"]) filters = [
["Purchase Order", "per_received", "<", "100"],
def get_purchase_order_item_supplied(po): ["Purchase Order", "is_subcontracted", "=", "Yes"],
return frappe.get_all("Purchase Order Item Supplied", filters=[ ["Purchase Order", "supplier", "=", filters.supplier],
('parent', 'IN', po) ["Purchase Order", "transaction_date", "<=", filters.to_date],
], fields=['parent', 'rm_item_code', 'required_qty']) ["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): class TestSubcontractedItemToBeTransferred(unittest.TestCase):
def test_pending_and_transferred_qty(self): 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', 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) 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, 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)), '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))})) '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]) po.reload()
self.assertIn(data[0]['t_qty'], [1, 2])
self.assertEqual(data[1]['purchase_order'], po.name) po_data = [row for row in data if row.get('purchase_order') == po.name]
self.assertIn(data[1]['rm_item_code'], ['_Test Item', '_Test Item Home Desktop 100']) # Alphabetically sort to be certain of order
self.assertIn(data[1]['p_qty'], [9, 18]) po_data = sorted(po_data, key = lambda i: i['rm_item_code'])
self.assertIn(data[1]['t_qty'], [1, 2])
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): 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 = [ 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'}, 'name': po.supplied_items[0].name,
{'item_code': '_Test Item Home Desktop 100', 'rm_item_code': '_Test Item Home Desktop 100', 'item_name': '_Test Item Home Desktop 100', 'qty': 2, 'item_code': item_1,
'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 200, 'stock_uom': 'Nos'}] '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) rm_item_string = json.dumps(rm_item)
se = frappe.get_doc(make_rm_stock_entry(po, rm_item_string)) se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string))
se.from_warehouse = '_Test Warehouse 1 - _TC' se.from_warehouse = '_Test Warehouse - _TC'
se.to_warehouse = '_Test Warehouse 1 - _TC' se.to_warehouse = '_Test Warehouse - _TC'
se.stock_entry_type = 'Send to Subcontractor' se.stock_entry_type = 'Send to Subcontractor'
se.save() se.save()
se.submit() 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_field = "purchase_order"
order_doctype = "Purchase Order" order_doctype = "Purchase Order"
order_list = list(set([d.get(order_field) order_list = list(set(d.get(order_field)
for d in self.get("items") if 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, journal_entries = get_advance_journal_entries(party_type, party, party_account,
amount_field, order_doctype, order_list, include_unallocated) amount_field, order_doctype, order_list, include_unallocated)
@ -635,8 +635,8 @@ class AccountsController(TransactionBase):
def validate_advance_entries(self): def validate_advance_entries(self):
order_field = "sales_order" if self.doctype == "Sales Invoice" else "purchase_order" order_field = "sales_order" if self.doctype == "Sales Invoice" else "purchase_order"
order_list = list(set([d.get(order_field) order_list = list(set(d.get(order_field)
for d in self.get("items") if d.get(order_field)])) for d in self.get("items") if d.get(order_field)))
if not order_list: return if not order_list: return
@ -828,6 +828,12 @@ class AccountsController(TransactionBase):
role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill') 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(): if total_billed_amt - max_allowed_amt > 0.01 and role_allowed_to_over_bill not in frappe.get_roles():
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") 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)) .format(item.item_code, item.idx, max_allowed_amt))

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.stock.get_item_details import get_conversion_factor
from erpnext.buying.utils import validate_for_items, update_last_purchase_rate from erpnext.buying.utils import validate_for_items, update_last_purchase_rate
from erpnext.stock.stock_ledger import get_valuation_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 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 frappe.contacts.doctype.address.address import get_address_display
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget 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.controllers.sales_and_purchase_return import get_rate_for_return
from erpnext.stock.utils import get_incoming_rate 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): def get_feed(self):
if self.get("supplier_name"): if self.get("supplier_name"):
@ -57,6 +58,11 @@ class BuyingController(StockController):
if self.doctype in ("Purchase Receipt", "Purchase Invoice"): if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
self.update_valuation_rate() 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): def set_missing_values(self, for_validate=False):
super(BuyingController, self).set_missing_values(for_validate) super(BuyingController, self).set_missing_values(for_validate)
@ -171,18 +177,19 @@ class BuyingController(StockController):
TODO: rename item_tax_amount to valuation_tax_amount 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 = self.get_stock_items() + self.get_asset_items()
stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0 stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0
last_item_idx = 1 last_item_idx = 1
for d in self.get("items"): 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_qty += flt(d.qty)
stock_and_asset_items_amount += flt(d.base_net_amount) stock_and_asset_items_amount += flt(d.base_net_amount)
last_item_idx = d.idx last_item_idx = d.idx
total_valuation_amount = sum([flt(d.base_tax_amount_after_discount_amount) for d in self.get("taxes") 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"]]) if d.category in ["Valuation", "Valuation and Total"])
valuation_amount_adjustment = total_valuation_amount valuation_amount_adjustment = total_valuation_amount
for i, item in enumerate(self.get("items")): for i, item in enumerate(self.get("items")):
@ -255,7 +262,7 @@ class BuyingController(StockController):
supplied_items_cost = 0.0 supplied_items_cost = 0.0
for d in self.get("supplied_items"): for d in self.get("supplied_items"):
if d.reference_name == item_row_id: 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({ rate = get_incoming_rate({
"item_code": d.rm_item_code, "item_code": d.rm_item_code,
"warehouse": self.supplier_warehouse, "warehouse": self.supplier_warehouse,
@ -285,11 +292,13 @@ class BuyingController(StockController):
if item in self.sub_contracted_items and not item.bom: 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)) frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code))
if self.doctype == "Purchase Order": if self.doctype != "Purchase Order":
for supplied_item in self.get("supplied_items"): return
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)))
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: else:
for item in self.get("items"): for item in self.get("items"):
if item.bom: if item.bom:
@ -297,23 +306,7 @@ class BuyingController(StockController):
def create_raw_materials_supplied(self, raw_material_table): def create_raw_materials_supplied(self, raw_material_table):
if self.is_subcontracted=="Yes": if self.is_subcontracted=="Yes":
parent_items = [] self.set_materials_for_subcontracted_items(raw_material_table)
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)
elif self.doctype in ["Purchase Receipt", "Purchase Invoice"]: elif self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
for item in self.get("items"): for item in self.get("items"):
@ -322,176 +315,6 @@ class BuyingController(StockController):
if self.is_subcontracted == "No" and self.get("supplied_items"): if self.is_subcontracted == "No" and self.get("supplied_items"):
self.set('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 @property
def sub_contracted_items(self): def sub_contracted_items(self):
if not hasattr(self, "_sub_contracted_items"): if not hasattr(self, "_sub_contracted_items"):
@ -683,6 +506,7 @@ class BuyingController(StockController):
self.process_fixed_asset() self.process_fixed_asset()
self.update_fixed_asset(field) self.update_fixed_asset(field)
if self.doctype in ['Purchase Order', 'Purchase Receipt']:
update_last_purchase_rate(self, is_submit = 1) update_last_purchase_rate(self, is_submit = 1)
def on_cancel(self): def on_cancel(self):
@ -691,7 +515,9 @@ class BuyingController(StockController):
if self.get('is_return'): if self.get('is_return'):
return return
if self.doctype in ['Purchase Order', 'Purchase Receipt']:
update_last_purchase_rate(self, is_submit = 0) update_last_purchase_rate(self, is_submit = 0)
if self.doctype in ['Purchase Receipt', 'Purchase Invoice']: if self.doctype in ['Purchase Receipt', 'Purchase Invoice']:
field = 'purchase_invoice' if self.doctype == 'Purchase Invoice' else 'purchase_receipt' field = 'purchase_invoice' if self.doctype == 'Purchase Invoice' else 'purchase_receipt'
@ -863,104 +689,6 @@ class BuyingController(StockController):
else: else:
validate_item_type(self, "is_purchase_item", "purchase") 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): def get_asset_item_details(asset_items):
asset_items_data = {} asset_items_data = {}
for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"], 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) 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) 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"]) fields = get_fields("Employee", ["name", "employee_name"])
return frappe.db.sql("""select {fields} from `tabEmployee` return frappe.db.sql("""select {fields} from `tabEmployee`
where status = 'Active' where status in ('Active', 'Suspended')
and docstatus < 2 and docstatus < 2
and ({key} like %(txt)s and ({key} like %(txt)s
or employee_name 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) fields = get_fields("Customer", fields)
searchfields = frappe.get_meta("Customer").get_search_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` return frappe.db.sql("""select {fields} from `tabCustomer`
where docstatus < 2 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` return frappe.db.sql("""select {fields} from `tabProject`
where where
`tabProject`.status not in ("Completed", "Cancelled") `tabProject`.status not in ("Completed", "Cancelled")
and {cond} {match_cond} {scond} and {cond} {scond} {match_cond}
order by order by
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
idx desc, idx desc,

View File

@ -330,9 +330,15 @@ class SellingController(StockController):
# For internal transfers use incoming rate as the valuation rate # For internal transfers use incoming rate as the valuation rate
if self.is_internal_transfer(): if self.is_internal_transfer():
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')) rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate'))
if d.rate != rate: if d.rate != rate:
d.rate = rate d.rate = rate
d.discount_percentage = 0 d.discount_percentage = 0
d.discount_amount = 0 d.discount_amount = 0
frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer") 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(',')))) 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): 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: 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')] 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']) args['name'] = self.get(args['percent_join_field_parent'])
self._update_percent_field(args, update_modified) self._update_percent_field(args, update_modified)
else: else:
distinct_transactions = set([d.get(args['percent_join_field']) distinct_transactions = set(d.get(args['percent_join_field'])
for d in self.get_all_children(args['source_dt'])]) for d in self.get_all_children(args['source_dt']))
for name in distinct_transactions: for name in distinct_transactions:
if name: if name:

View File

@ -11,7 +11,7 @@ from frappe.utils import cint, cstr, flt, get_link_to_form, getdate
import erpnext import erpnext
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map 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.controllers.accounts_controller import AccountsController
from erpnext.stock import get_warehouse_account_map from erpnext.stock import get_warehouse_account_map
from erpnext.stock.stock_ledger import get_valuation_rate from erpnext.stock.stock_ledger import get_valuation_rate
@ -313,7 +313,7 @@ class StockController(AccountsController):
def get_serialized_items(self): def get_serialized_items(self):
serialized_items = [] 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: if item_codes:
serialized_items = frappe.db.sql_list("""select name from `tabItem` serialized_items = frappe.db.sql_list("""select name from `tabItem`
where has_serial_no=1 and name in ({})""".format(", ".join(["%s"]*len(item_codes))), 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): def validate_warehouse(self):
from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company
warehouses = list(set([d.warehouse for d in warehouses = list(set(d.warehouse for d in
self.get("items") if getattr(d, "warehouse", None)])) self.get("items") if getattr(d, "warehouse", None)))
target_warehouses = list(set([d.target_warehouse for d in target_warehouses = list(set([d.target_warehouse for d in
self.get("items") if getattr(d, "target_warehouse", None)])) self.get("items") if getattr(d, "target_warehouse", None)]))
@ -497,10 +497,6 @@ class StockController(AccountsController):
}) })
if future_sle_exists(args): if future_sle_exists(args):
create_repost_item_valuation_entry(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() @frappe.whitelist()
def make_quality_inspections(doctype, docname, items): def make_quality_inspections(doctype, docname, items):
@ -533,21 +529,75 @@ def make_quality_inspections(doctype, docname, items):
return inspections return inspections
def is_reposting_pending(): def is_reposting_pending():
return frappe.db.exists("Repost Item Valuation", return frappe.db.exists("Repost Item Valuation",
{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) {'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): if validate_future_sle_not_exists(args, key, sl_entries):
sl_entries = frappe.get_all("Stock Ledger Entry", 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}, filters={"voucher_type": args.voucher_type, "voucher_no": args.voucher_no},
fields=["item_code", "warehouse"], fields=["item_code", "warehouse"],
order_by="creation asc") order_by="creation asc")
if not sl_entries: def get_conditions_to_validate_future_sle(sl_entries):
return
warehouse_items_map = {} warehouse_items_map = {}
for entry in sl_entries: for entry in sl_entries:
if entry.warehouse not in warehouse_items_map: if entry.warehouse not in warehouse_items_map:
@ -558,23 +608,10 @@ def future_sle_exists(args):
or_conditions = [] or_conditions = []
for warehouse, items in warehouse_items_map.items(): for warehouse, items in warehouse_items_map.items():
or_conditions.append( or_conditions.append(
"warehouse = '{}' and item_code in ({})".format( f"""warehouse = {frappe.db.escape(warehouse)}
warehouse, and item_code in ({', '.join(frappe.db.escape(item) for item in items)})""")
", ".join(frappe.db.escape(item) for item in items)
)
)
return frappe.db.sql(""" return or_conditions
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)
def create_repost_item_valuation_entry(args): def create_repost_item_valuation_entry(args):
args = frappe._dict(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): def manipulate_grand_total_for_inclusive_tax(self):
# if fully inclusive taxes and diff # 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] last_tax = self.doc.get("taxes")[-1]
non_inclusive_tax_amount = sum([flt(d.tax_amount_after_discount_amount) 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]) for d in self.doc.get("taxes") if not d.included_in_print_rate)
diff = self.doc.total + non_inclusive_tax_amount \ diff = self.doc.total + non_inclusive_tax_amount \
- flt(last_tax.total, last_tax.precision("total")) - flt(last_tax.total, last_tax.precision("total"))
@ -521,8 +521,8 @@ class calculate_taxes_and_totals(object):
def calculate_total_advance(self): def calculate_total_advance(self):
if self.doc.docstatus < 2: if self.doc.docstatus < 2:
total_allocated_amount = sum([flt(adv.allocated_amount, adv.precision("allocated_amount")) total_allocated_amount = sum(flt(adv.allocated_amount, adv.precision("allocated_amount"))
for adv in self.doc.get("advances")]) for adv in self.doc.get("advances"))
self.doc.total_advance = flt(total_allocated_amount, self.doc.precision("total_advance")) 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" \ if self.doc.doctype == "Sales Invoice" \
and self.doc.paid_amount > self.doc.grand_total and not self.doc.is_return \ 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 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 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_type = None
item.margin_rate_or_amount = 0.0 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 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) rate_with_margin = flt(item.price_list_rate) + flt(margin_value)
base_rate_with_margin = flt(rate_with_margin) * flt(self.doc.conversion_rate) 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 # Assert that all inserted items are present in updated sales order
src_items = item_list_1 + item_list_2 + item_list_3 src_items = item_list_1 + item_list_2 + item_list_3
self.assertEqual(set([d for d in src_items]), self.assertEqual(set(d for d in src_items),
set([d.item_code for d in updated_so.items])) set(d.item_code for d in updated_so.items))
def make_quotation(self, item_list, customer): def make_quotation(self, item_list, customer):

View File

@ -113,7 +113,7 @@ def post_process(doctype, data):
doc.set_indicator() doc.set_indicator()
doc.status_display = ", ".join(doc.status_display) 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) result.append(doc)
return result return result

View File

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

View File

@ -6,7 +6,8 @@ frappe.ui.form.on('Assessment Result', {
if (!frm.doc.__islocal) { if (!frm.doc.__islocal) {
frm.trigger('setup_chart'); 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() { frm.set_query('course', function() {
return { return {

View File

@ -33,6 +33,7 @@ class Patient(Document):
self.reload() # self.notify_update() self.reload() # self.notify_update()
def on_update(self): def on_update(self):
if frappe.db.get_single_value('Healthcare Settings', 'link_customer_to_patient'):
if self.customer: if self.customer:
customer = frappe.get_doc('Customer', self.customer) customer = frappe.get_doc('Customer', self.customer)
if self.customer_group: if self.customer_group:
@ -47,7 +48,6 @@ class Patient(Document):
customer.ignore_mandatory = True customer.ignore_mandatory = True
customer.save(ignore_permissions=True) customer.save(ignore_permissions=True)
else: else:
if frappe.db.get_single_value('Healthcare Settings', 'link_customer_to_patient'):
create_customer(self) create_customer(self)
def set_full_name(self): def set_full_name(self):

View File

@ -15,6 +15,7 @@ class Attendance(Document):
validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"]) validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"])
self.validate_attendance_date() self.validate_attendance_date()
self.validate_duplicate_record() self.validate_duplicate_record()
self.validate_employee_status()
self.check_leave_record() self.check_leave_record()
def validate_attendance_date(self): 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.throw(_("Attendance for employee {0} is already marked for the date {1}").format(
frappe.bold(self.employee), frappe.bold(self.attendance_date))) 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): def check_leave_record(self):
leave_record = frappe.db.sql(""" leave_record = frappe.db.sql("""
select leave_type, half_day, half_day_date select leave_type, half_day, half_day_date

View File

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

View File

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

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe 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.model.naming import set_name_by_naming_series
from frappe import throw, _, scrub from frappe import throw, _, scrub
from frappe.permissions import add_user_permission, remove_user_permission, \ 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 frappe.model.document import Document
from erpnext.utilities.transaction_base import delete_events from erpnext.utilities.transaction_base import delete_events
from frappe.utils.nestedset import NestedSet 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 EmployeeUserDisabledError(frappe.ValidationError): pass
class EmployeeLeftValidationError(frappe.ValidationError): pass class EmployeeLeftValidationError(frappe.ValidationError): pass
@ -37,7 +36,7 @@ class Employee(NestedSet):
def validate(self): def validate(self):
from erpnext.controllers.status_updater import validate_status 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.employee = self.name
self.set_employee_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'), 'heatmap_message': _('This is based on the attendance of this Employee'),
'fieldname': 'employee', 'fieldname': 'employee',
'non_standard_fieldnames': { 'non_standard_fieldnames': {
'Bank Account': 'party' 'Bank Account': 'party',
'Employee Grievance': 'raised_by'
}, },
'transactions': [ 'transactions': [
{ {
@ -20,7 +21,7 @@ def get_data():
}, },
{ {
'label': _('Lifecycle'), 'label': _('Lifecycle'),
'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation'] 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance']
}, },
{ {
'label': _('Shift'), 'label': _('Shift'),

View File

@ -3,7 +3,7 @@ frappe.listview_settings['Employee'] = {
filters: [["status","=", "Active"]], filters: [["status","=", "Active"]],
get_indicator: function(doc) { get_indicator: function(doc) {
var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status]; 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; 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 // MIT License. See license.txt
frappe.listview_settings['Job Applicant'] = { frappe.listview_settings['Job Applicant'] = {
add_fields: ["company", "designation", "job_applicant", "status"], add_fields: ["status"],
get_indicator: function (doc) { get_indicator: function (doc) {
if (doc.status == "Accepted") { if (doc.status == "Accepted") {
return [__(doc.status), "green", "status,=," + doc.status]; return [__(doc.status), "green", "status,=," + doc.status];

View File

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

View File

@ -8,6 +8,7 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import set_employee_name, get_leave_period 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_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 OverlapError(frappe.ValidationError): pass
class BackDatedAllocationError(frappe.ValidationError): pass class BackDatedAllocationError(frappe.ValidationError): pass
@ -55,6 +56,43 @@ class LeaveAllocation(Document):
if self.carry_forward: if self.carry_forward:
self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True) 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): def update_leave_policy_assignments_when_no_allocations_left(self):
allocations = frappe.db.get_list("Leave Allocation", filters = { allocations = frappe.db.get_list("Leave Allocation", filters = {
"docstatus": 1, "docstatus": 1,

View File

@ -1,5 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
import erpnext
import unittest import unittest
from frappe.utils import nowdate, add_months, getdate, add_days from frappe.utils import nowdate, add_months, getdate, add_days
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
@ -164,6 +165,51 @@ class TestLeaveAllocation(unittest.TestCase):
leave_allocation.cancel() leave_allocation.cancel()
self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name})) 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): def create_leave_allocation(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@ -41,7 +41,7 @@ class StaffingPlan(Document):
detail.total_estimated_cost = 0 detail.total_estimated_cost = 0
if detail.number_of_positions > 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) detail.total_estimated_cost = cint(detail.vacancies) * flt(detail.estimated_cost_per_position)
self.total_estimated_budget += detail.total_estimated_cost 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 \ 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): 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} \ frappe.throw(_("You can only plan for upto {0} vacancies and budget {1} \
for {2} as per staffing plan {3} for parent company {4}." for {2} as per staffing plan {3} for parent company {4}.").format(
.format(cint(parent_plan_details[0].vacancies), cint(parent_plan_details[0].vacancies),
parent_plan_details[0].total_estimated_cost, parent_plan_details[0].total_estimated_cost,
frappe.bold(staffing_plan_detail.designation), frappe.bold(staffing_plan_detail.designation),
parent_plan_details[0].name, parent_plan_details[0].name,
parent_company)), ParentCompanyError) parent_company), ParentCompanyError)
#Get vacanices already planned for all companies down the hierarchy of Parent Company #Get vacanices already planned for all companies down the hierarchy of Parent Company
lft, rgt = frappe.get_cached_value('Company', parent_company, ["lft", "rgt"]) 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(parent_plan_details[0].total_estimated_cost) < \
(flt(staffing_plan_detail.total_estimated_cost) + flt(all_sibling_details.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}. \ 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}." You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}.").format(
.format(cint(all_sibling_details.vacancies), cint(all_sibling_details.vacancies),
all_sibling_details.total_estimated_cost, all_sibling_details.total_estimated_cost,
frappe.bold(staffing_plan_detail.designation), frappe.bold(staffing_plan_detail.designation),
parent_company, parent_company,
cint(parent_plan_details[0].vacancies), cint(parent_plan_details[0].vacancies),
parent_plan_details[0].total_estimated_cost, 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): def validate_with_subsidiary_plans(self, staffing_plan_detail):
#Valdate this plan with all child company plan #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 \ cint(staffing_plan_detail.vacancies) < cint(children_details.vacancies) or \
flt(staffing_plan_detail.total_estimated_cost) < flt(children_details.total_estimated_cost): 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}. \ 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" Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies").format(
.format(self.company, self.company,
cint(children_details.vacancies), cint(children_details.vacancies),
children_details.total_estimated_cost, children_details.total_estimated_cost,
frappe.bold(staffing_plan_detail.designation))), SubsidiaryCompanyError) frappe.bold(staffing_plan_detail.designation)), SubsidiaryCompanyError)
@frappe.whitelist() @frappe.whitelist()
def get_designation_counts(designation, company): def get_designation_counts(designation, company):

View File

@ -11,8 +11,8 @@
"event": "Submit", "event": "Submit",
"idx": 0, "idx": 0,
"is_standard": 1, "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>", "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-05-24 16:29:13.165930", "modified": "2021-06-16 14:08:12.933367",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Training Scheduled", "name": "Training Scheduled",

View File

@ -29,14 +29,14 @@
{{_("Timing")}}: <b>{{ start.strftime("%I:%M %p") + ' to ' + end.strftime("%I:%M %p") }}</b> {{_("Timing")}}: <b>{{ start.strftime("%I:%M %p") + ' to ' + end.strftime("%I:%M %p") }}</b>
</li> </li>
{% else %} {% else %}
<li>{{_("Start Time")}}: <b>{{ start.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>{{_("End Time")}}: <b>{{ end.strftime("%A, %d %b %Y at %I:%M %p") }}</b>
</li> </li>
<li>{{_("End Time")}}: <b>{{ end.strftime("%A, %d %b %Y at %I:%M %p") }}</b></li>
{% endif %} {% 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 %} {% if doc.is_mandatory %}
<li>Note: This Training Event is mandatory</li> <li>{{ _("Note: This Training Event is mandatory") }}</li>
{% endif %} {% endif %}
</ul> </ul>
</div> </div>

View File

@ -178,7 +178,7 @@ def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type):
is_carry_forward, is_expired is_carry_forward, is_expired
FROM `tabLeave Ledger Entry` FROM `tabLeave Ledger Entry`
WHERE employee=%(employee)s AND leave_type=%(leave_type)s 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 AND (from_date between %(from_date)s AND %(to_date)s
OR to_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)) OR (from_date < %(from_date)s AND to_date > %(to_date)s))

View File

@ -153,6 +153,24 @@
"onboard": 0, "onboard": 0,
"type": "Link" "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", "dependencies": "Employee",
"hidden": 0, "hidden": 0,
@ -823,7 +841,7 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2021-04-26 13:36:15.413819", "modified": "2021-05-13 17:19:40.524444",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "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) 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): 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) 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): 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))) 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) frappe.db.set_value("Loan", doc.name, "total_amount_paid", total_amount_paid)
def get_total_loan_amount(applicant_type, applicant, company): def get_total_loan_amount(applicant_type, applicant, company):
return frappe.db.get_value('Loan', pending_amount = 0
{'applicant_type': applicant_type, 'company': company, 'applicant': applicant, 'docstatus': 1}, loan_details = frappe.db.get_all("Loan",
'sum(loan_amount)') 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): def get_sanctioned_amount_limit(applicant_type, applicant, company):
return frappe.db.get_value('Sanctioned Loan Amount', 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"): if not frappe.db.exists("Customer", "_Test Loan Customer"):
frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True) 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) 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_entries1)
self.assertTrue(gl_entries2) 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): def test_regular_loan_repayment(self):
pledge = [{ pledge = [{
"loan_security": "Test Security 1", "loan_security": "Test Security 1",
@ -367,7 +403,7 @@ class TestLoan(unittest.TestCase):
unpledge_request.load_from_db() unpledge_request.load_from_db()
self.assertEqual(unpledge_request.docstatus, 1) self.assertEqual(unpledge_request.docstatus, 1)
def test_santined_loan_security_unpledge(self): def test_sanctioned_loan_security_unpledge(self):
pledge = [{ pledge = [{
"loan_security": "Test Security 1", "loan_security": "Test Security 1",
"qty": 4000.00 "qty": 4000.00
@ -858,7 +894,7 @@ def create_repayment_entry(loan, applicant, posting_date, paid_amount):
return lr return lr
def create_loan_application(company, applicant, loan_type, proposed_pledges, repayment_method=None, 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 = frappe.new_doc('Loan Application')
loan_application.applicant_type = 'Customer' loan_application.applicant_type = 'Customer'
loan_application.company = company loan_application.company = company
@ -874,6 +910,9 @@ def create_loan_application(company, applicant, loan_type, proposed_pledges, rep
for pledge in proposed_pledges: for pledge in proposed_pledges:
loan_application.append('proposed_pledges', pledge) loan_application.append('proposed_pledges', pledge)
if do_not_save:
return loan_application
loan_application.save() loan_application.save()
loan_application.submit() 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)) 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): 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) 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): 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))) frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant)))

View File

@ -235,6 +235,7 @@ class LoanRepayment(AccountsController):
else: else:
remarks = _("Repayment against Loan: ") + self.against_loan remarks = _("Repayment against Loan: ") + self.against_loan
if not loan_details.repay_from_salary:
if self.total_penalty_paid: if self.total_penalty_paid:
gle_map.append( gle_map.append(
self.get_gl_dict({ self.get_gl_dict({

View File

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

View File

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

View File

@ -1,7 +1,8 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals from typing import List
from collections import deque
import frappe, erpnext import frappe, erpnext
from frappe.utils import cint, cstr, flt, today from frappe.utils import cint, cstr, flt, today
from frappe import _ from frappe import _
@ -16,14 +17,85 @@ from frappe.model.mapper import get_mapped_doc
import functools import functools
from six import string_types
from operator import itemgetter from operator import itemgetter
form_grid_templates = { form_grid_templates = {
"items": "templates/form_grid/item_grid.html" "items": "templates/form_grid/item_grid.html"
} }
class BOMTree:
"""Full tree representation of a BOM"""
# specifying the attributes to save resources
# ref: https://docs.python.org/3/reference/datamodel.html#slots
__slots__ = ["name", "child_items", "is_bom", "item_code", "exploded_qty", "qty"]
def __init__(self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1) -> None:
self.name = name # name of node, BOM number if is_bom else item_code
self.child_items: List["BOMTree"] = [] # list of child items
self.is_bom = is_bom # true if the node is a BOM and not a leaf item
self.item_code: str = None # item_code associated with node
self.qty = qty # required unit quantity to make one unit of parent item.
self.exploded_qty = exploded_qty # total exploded qty required for making root of tree.
if not self.is_bom:
self.item_code = self.name
else:
self.__create_tree()
def __create_tree(self):
bom = frappe.get_cached_doc("BOM", self.name)
self.item_code = bom.item
for item in bom.get("items", []):
qty = item.qty / bom.quantity # quantity per unit
exploded_qty = self.exploded_qty * qty
if item.bom_no:
child = BOMTree(item.bom_no, exploded_qty=exploded_qty, qty=qty)
self.child_items.append(child)
else:
self.child_items.append(
BOMTree(item.item_code, is_bom=False, exploded_qty=exploded_qty, qty=qty)
)
def level_order_traversal(self) -> List["BOMTree"]:
"""Get level order traversal of tree.
E.g. for following tree the traversal will return list of nodes in order from top to bottom.
BOM:
- SubAssy1
- item1
- item2
- SubAssy2
- item3
- item4
returns = [SubAssy1, item1, item2, SubAssy2, item3, item4]
"""
traversal = []
q = deque()
q.append(self)
while q:
node = q.popleft()
for child in node.child_items:
traversal.append(child)
q.append(child)
return traversal
def __str__(self) -> str:
return (
f"{self.item_code}{' - ' + self.name if self.is_bom else ''} qty(per unit): {self.qty}"
f" exploded_qty: {self.exploded_qty}"
)
def __repr__(self, level: int = 0) -> str:
rep = "" * (level - 1) + "┣━ " * (level > 0) + str(self) + "\n"
for child in self.child_items:
rep += child.__repr__(level=level + 1)
return rep
class BOM(WebsiteGenerator): class BOM(WebsiteGenerator):
website = frappe._dict( website = frappe._dict(
# page_title_field = "item_name", # page_title_field = "item_name",
@ -81,7 +153,7 @@ class BOM(WebsiteGenerator):
self.validate_operations() self.validate_operations()
self.calculate_cost() self.calculate_cost()
self.update_stock_qty() self.update_stock_qty()
self.update_cost(update_parent=False, from_child_bom=True, save=False) self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False)
def get_context(self, context): def get_context(self, context):
context.parents = [{'name': 'boms', 'title': _('All BOMs') }] context.parents = [{'name': 'boms', 'title': _('All BOMs') }]
@ -152,7 +224,7 @@ class BOM(WebsiteGenerator):
if not args: if not args:
args = frappe.form_dict.get('args') args = frappe.form_dict.get('args')
if isinstance(args, string_types): if isinstance(args, str):
import json import json
args = json.loads(args) args = json.loads(args)
@ -213,7 +285,7 @@ class BOM(WebsiteGenerator):
return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1) return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1)
@frappe.whitelist() @frappe.whitelist()
def update_cost(self, update_parent=True, from_child_bom=False, save=True): def update_cost(self, update_parent=True, from_child_bom=False, update_hour_rate = True, save=True):
if self.docstatus == 2: if self.docstatus == 2:
return return
@ -242,7 +314,7 @@ class BOM(WebsiteGenerator):
if self.docstatus == 1: if self.docstatus == 1:
self.flags.ignore_validate_update_after_submit = True self.flags.ignore_validate_update_after_submit = True
self.calculate_cost() self.calculate_cost(update_hour_rate)
if save: if save:
self.db_update() self.db_update()
@ -403,32 +475,47 @@ class BOM(WebsiteGenerator):
bom_list.reverse() bom_list.reverse()
return bom_list return bom_list
def calculate_cost(self): def calculate_cost(self, update_hour_rate = False):
"""Calculate bom totals""" """Calculate bom totals"""
self.calculate_op_cost() self.calculate_op_cost(update_hour_rate)
self.calculate_rm_cost() self.calculate_rm_cost()
self.calculate_sm_cost() self.calculate_sm_cost()
self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost
self.base_total_cost = self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost self.base_total_cost = self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost
def calculate_op_cost(self): def calculate_op_cost(self, update_hour_rate = False):
"""Update workstation rate and calculates totals""" """Update workstation rate and calculates totals"""
self.operating_cost = 0 self.operating_cost = 0
self.base_operating_cost = 0 self.base_operating_cost = 0
for d in self.get('operations'): for d in self.get('operations'):
if d.workstation: if d.workstation:
if not d.hour_rate: self.update_rate_and_time(d, update_hour_rate)
hour_rate = flt(frappe.db.get_value("Workstation", d.workstation, "hour_rate"))
d.hour_rate = hour_rate / flt(self.conversion_rate) if self.conversion_rate else hour_rate
if d.hour_rate and d.time_in_mins:
d.base_hour_rate = flt(d.hour_rate) * flt(self.conversion_rate)
d.operating_cost = flt(d.hour_rate) * flt(d.time_in_mins) / 60.0
d.base_operating_cost = flt(d.operating_cost) * flt(self.conversion_rate)
self.operating_cost += flt(d.operating_cost) self.operating_cost += flt(d.operating_cost)
self.base_operating_cost += flt(d.base_operating_cost) self.base_operating_cost += flt(d.base_operating_cost)
def update_rate_and_time(self, row, update_hour_rate = False):
if not row.hour_rate or update_hour_rate:
hour_rate = flt(frappe.get_cached_value("Workstation", row.workstation, "hour_rate"))
row.hour_rate = (hour_rate / flt(self.conversion_rate)
if self.conversion_rate and hour_rate else hour_rate)
if self.routing:
row.time_in_mins = flt(frappe.db.get_value("BOM Operation", {
"workstation": row.workstation,
"operation": row.operation,
"sequence_id": row.sequence_id,
"parent": self.routing
}, ["time_in_mins"]))
if row.hour_rate and row.time_in_mins:
row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate)
row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0
row.base_operating_cost = flt(row.operating_cost) * flt(self.conversion_rate)
if update_hour_rate:
row.db_update()
def calculate_rm_cost(self): def calculate_rm_cost(self):
"""Fetch RM rate as per today's valuation rate and calculate totals""" """Fetch RM rate as per today's valuation rate and calculate totals"""
total_rm_cost = 0 total_rm_cost = 0
@ -575,7 +662,7 @@ class BOM(WebsiteGenerator):
self.get_routing() self.get_routing()
def validate_operations(self): def validate_operations(self):
if self.with_operations and not self.get('operations'): if self.with_operations and not self.get('operations') and self.docstatus == 1:
frappe.throw(_("Operations cannot be left blank")) frappe.throw(_("Operations cannot be left blank"))
if self.with_operations: if self.with_operations:
@ -585,6 +672,11 @@ class BOM(WebsiteGenerator):
if not d.batch_size or d.batch_size <= 0: if not d.batch_size or d.batch_size <= 0:
d.batch_size = 1 d.batch_size = 1
def get_tree_representation(self) -> BOMTree:
"""Get a complete tree representation preserving order of child items."""
return BOMTree(self.name)
def get_bom_item_rate(args, bom_doc): def get_bom_item_rate(args, bom_doc):
if bom_doc.rm_cost_as_per == 'Valuation Rate': if bom_doc.rm_cost_as_per == 'Valuation Rate':
rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1) rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1)

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