Merge branch 'staging-fixes' into staging
This commit is contained in:
commit
815c575a0b
11
.travis.yml
11
.travis.yml
@ -33,17 +33,16 @@ before_script:
|
|||||||
- cd ~/frappe-bench
|
- cd ~/frappe-bench
|
||||||
- bench get-app erpnext $TRAVIS_BUILD_DIR
|
- bench get-app erpnext $TRAVIS_BUILD_DIR
|
||||||
- bench use test_site
|
- bench use test_site
|
||||||
- bench reinstall --mariadb-root-username root --mariadb-root-password travis --yes
|
|
||||||
- bench build
|
|
||||||
- bench scheduler disable
|
|
||||||
- sed -i 's/9000/9001/g' sites/common_site_config.json
|
|
||||||
- bench start &
|
|
||||||
- sleep 10
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
include:
|
include:
|
||||||
- stage: test
|
- stage: test
|
||||||
script:
|
script:
|
||||||
|
- bench reinstall --mariadb-root-username root --mariadb-root-password travis --yes
|
||||||
|
- bench scheduler disable
|
||||||
|
- sed -i 's/9000/9001/g' sites/common_site_config.json
|
||||||
|
- bench start &
|
||||||
|
- sleep 10
|
||||||
- set -e
|
- set -e
|
||||||
- bench run-tests --app erpnext --coverage
|
- bench run-tests --app erpnext --coverage
|
||||||
after_script:
|
after_script:
|
||||||
|
@ -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__ = '10.1.76'
|
__version__ = '10.1.78'
|
||||||
|
|
||||||
def get_default_company(user=None):
|
def get_default_company(user=None):
|
||||||
'''Get default company for user'''
|
'''Get default company for user'''
|
||||||
|
@ -39,6 +39,8 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
|
|||||||
});
|
});
|
||||||
frm.events.get_total_gain_loss(frm);
|
frm.events.get_total_gain_loss(frm);
|
||||||
refresh_field("accounts");
|
refresh_field("accounts");
|
||||||
|
} else {
|
||||||
|
frappe.msgprint(__("No records found"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -67,6 +67,8 @@ class ExchangeRateRevaluation(Document):
|
|||||||
and account_currency != %s
|
and account_currency != %s
|
||||||
order by name""",(self.company, company_currency))
|
order by name""",(self.company, company_currency))
|
||||||
|
|
||||||
|
account_details = []
|
||||||
|
if accounts:
|
||||||
account_details = frappe.db.sql("""
|
account_details = frappe.db.sql("""
|
||||||
select
|
select
|
||||||
account, party_type, party, account_currency,
|
account, party_type, party, account_currency,
|
||||||
|
@ -18,15 +18,14 @@ class GLEntry(Document):
|
|||||||
self.flags.ignore_submit_comment = True
|
self.flags.ignore_submit_comment = True
|
||||||
self.check_mandatory()
|
self.check_mandatory()
|
||||||
self.validate_and_set_fiscal_year()
|
self.validate_and_set_fiscal_year()
|
||||||
|
self.pl_must_have_cost_center()
|
||||||
|
self.validate_cost_center()
|
||||||
|
|
||||||
if not self.flags.from_repost:
|
if not self.flags.from_repost:
|
||||||
self.pl_must_have_cost_center()
|
|
||||||
self.check_pl_account()
|
self.check_pl_account()
|
||||||
self.validate_cost_center()
|
|
||||||
self.validate_party()
|
self.validate_party()
|
||||||
self.validate_currency()
|
self.validate_currency()
|
||||||
|
|
||||||
|
|
||||||
def on_update_with_args(self, adv_adj, update_outstanding = 'Yes', from_repost=False):
|
def on_update_with_args(self, adv_adj, update_outstanding = 'Yes', from_repost=False):
|
||||||
if not from_repost:
|
if not from_repost:
|
||||||
self.validate_account_details(adv_adj)
|
self.validate_account_details(adv_adj)
|
||||||
|
@ -206,6 +206,10 @@ class PurchaseInvoice(BuyingController):
|
|||||||
stock_not_billed_account = self.get_company_default("stock_received_but_not_billed")
|
stock_not_billed_account = self.get_company_default("stock_received_but_not_billed")
|
||||||
stock_items = self.get_stock_items()
|
stock_items = self.get_stock_items()
|
||||||
|
|
||||||
|
asset_items = [d.is_fixed_asset for d in self.items if d.is_fixed_asset]
|
||||||
|
if len(asset_items) > 0:
|
||||||
|
asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed")
|
||||||
|
|
||||||
if self.update_stock:
|
if self.update_stock:
|
||||||
self.validate_item_code()
|
self.validate_item_code()
|
||||||
self.validate_warehouse()
|
self.validate_warehouse()
|
||||||
@ -226,7 +230,8 @@ class PurchaseInvoice(BuyingController):
|
|||||||
item.expense_account = warehouse_account[item.warehouse]["account"]
|
item.expense_account = warehouse_account[item.warehouse]["account"]
|
||||||
else:
|
else:
|
||||||
item.expense_account = stock_not_billed_account
|
item.expense_account = stock_not_billed_account
|
||||||
|
elif item.is_fixed_asset and d.pr_detail:
|
||||||
|
item.expense_account = asset_received_but_not_billed
|
||||||
elif not item.expense_account and for_validate:
|
elif not item.expense_account and for_validate:
|
||||||
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
|
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
|
||||||
|
|
||||||
@ -360,7 +365,10 @@ class PurchaseInvoice(BuyingController):
|
|||||||
|
|
||||||
def get_gl_entries(self, warehouse_account=None):
|
def get_gl_entries(self, warehouse_account=None):
|
||||||
self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
|
self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
|
||||||
|
if self.auto_accounting_for_stock:
|
||||||
self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed")
|
self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed")
|
||||||
|
else:
|
||||||
|
self.stock_received_but_not_billed = None
|
||||||
self.expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
|
self.expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
|
||||||
self.negative_expense_to_be_booked = 0.0
|
self.negative_expense_to_be_booked = 0.0
|
||||||
gl_entries = []
|
gl_entries = []
|
||||||
|
@ -250,10 +250,12 @@ def get_serial_no_data(pos_profile, company):
|
|||||||
|
|
||||||
cond = "1=1"
|
cond = "1=1"
|
||||||
if pos_profile.get('update_stock') and pos_profile.get('warehouse'):
|
if pos_profile.get('update_stock') and pos_profile.get('warehouse'):
|
||||||
cond = "warehouse = '{0}'".format(pos_profile.get('warehouse'))
|
cond = "warehouse = %(warehouse)s"
|
||||||
|
|
||||||
serial_nos = frappe.db.sql("""select name, warehouse, item_code from `tabSerial No` where {0}
|
serial_nos = frappe.db.sql("""select name, warehouse, item_code
|
||||||
and company = %(company)s """.format(cond), {'company': company}, as_dict=1)
|
from `tabSerial No` where {0} and company = %(company)s """.format(cond),{
|
||||||
|
'company': company, 'warehouse': frappe.db.escape(pos_profile.get('warehouse'))
|
||||||
|
}, as_dict=1)
|
||||||
|
|
||||||
itemwise_serial_no = {}
|
itemwise_serial_no = {}
|
||||||
for sn in serial_nos:
|
for sn in serial_nos:
|
||||||
|
@ -1135,18 +1135,15 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
apply_category: function() {
|
apply_category: function() {
|
||||||
frappe.db.get_value("Item Group", {lft: 1, is_group: 1}, "name", (r) => {
|
var me = this;
|
||||||
category = this.selected_item_group || r.name;
|
category = this.selected_item_group || "All Item Groups";
|
||||||
|
if(category == 'All Item Groups') {
|
||||||
if(category == r.name) {
|
|
||||||
return this.item_data
|
return this.item_data
|
||||||
} else {
|
} else {
|
||||||
return this.item_data.filter(function(element, index, array){
|
return this.item_data.filter(function(element, index, array){
|
||||||
return element.item_group == category;
|
return element.item_group == category;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
bind_items_event: function() {
|
bind_items_event: function() {
|
||||||
|
@ -190,7 +190,7 @@ class ReceivablePayableReport(object):
|
|||||||
dn_details = get_dn_details(args.get("party_type"), voucher_nos)
|
dn_details = get_dn_details(args.get("party_type"), voucher_nos)
|
||||||
self.voucher_details = get_voucher_details(args.get("party_type"), voucher_nos, dn_details)
|
self.voucher_details = get_voucher_details(args.get("party_type"), voucher_nos, dn_details)
|
||||||
|
|
||||||
if self.filters.based_on_payment_terms:
|
if self.filters.based_on_payment_terms and gl_entries_data:
|
||||||
self.payment_term_map = self.get_payment_term_detail(voucher_nos)
|
self.payment_term_map = self.get_payment_term_detail(voucher_nos)
|
||||||
|
|
||||||
for gle in gl_entries_data:
|
for gle in gl_entries_data:
|
||||||
|
@ -180,7 +180,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
|||||||
def get_voucherwise_data(self, party_naming_by, args):
|
def get_voucherwise_data(self, party_naming_by, args):
|
||||||
voucherwise_data = ReceivablePayableReport(self.filters).run(args)[1]
|
voucherwise_data = ReceivablePayableReport(self.filters).run(args)[1]
|
||||||
|
|
||||||
cols = ["posting_date", "party"]
|
cols = ["posting_date", "party", "customer-contact"]
|
||||||
|
|
||||||
if party_naming_by == "Naming Series":
|
if party_naming_by == "Naming Series":
|
||||||
cols += ["party_name"]
|
cols += ["party_name"]
|
||||||
|
@ -35,16 +35,22 @@ def get_conditions(filters):
|
|||||||
|
|
||||||
def get_entries(filters):
|
def get_entries(filters):
|
||||||
conditions = get_conditions(filters)
|
conditions = get_conditions(filters)
|
||||||
journal_entries = frappe.db.sql("""select "Journal Entry", jv.name, jv.posting_date,
|
journal_entries = frappe.db.sql("""SELECT
|
||||||
jv.cheque_no, jv.clearance_date, jvd.against_account, (jvd.debit - jvd.credit)
|
"Journal Entry", jv.name, jv.posting_date, jv.cheque_no, jv.clearance_date, jvd.against_account,
|
||||||
from `tabJournal Entry Account` jvd, `tabJournal Entry` jv
|
if((jvd.debit - jvd.credit) < 0, (jvd.debit - jvd.credit) * -1, (jvd.debit - jvd.credit))
|
||||||
where jvd.parent = jv.name and jv.docstatus=1 and jvd.account = %(account)s {0}
|
FROM
|
||||||
|
`tabJournal Entry Account` jvd, `tabJournal Entry` jv
|
||||||
|
WHERE
|
||||||
|
jvd.parent = jv.name and jv.docstatus=1 and jvd.account = %(account)s {0}
|
||||||
order by posting_date DESC, jv.name DESC""".format(conditions), filters, as_list=1)
|
order by posting_date DESC, jv.name DESC""".format(conditions), filters, as_list=1)
|
||||||
|
|
||||||
payment_entries = frappe.db.sql("""select "Payment Entry", name, posting_date,
|
payment_entries = frappe.db.sql("""SELECT
|
||||||
reference_no, clearance_date, party, if(paid_from=%(account)s, paid_amount, received_amount)
|
"Payment Entry", name, posting_date, reference_no, clearance_date, party,
|
||||||
from `tabPayment Entry`
|
if(paid_from=%(account)s, paid_amount, received_amount)
|
||||||
where docstatus=1 and (paid_from = %(account)s or paid_to = %(account)s) {0}
|
FROM
|
||||||
|
`tabPayment Entry`
|
||||||
|
WHERE
|
||||||
|
docstatus=1 and (paid_from = %(account)s or paid_to = %(account)s) {0}
|
||||||
order by posting_date DESC, name DESC""".format(conditions), filters, as_list=1)
|
order by posting_date DESC, name DESC""".format(conditions), filters, as_list=1)
|
||||||
|
|
||||||
return sorted(journal_entries + payment_entries, key=lambda k: k[2] or getdate(nowdate()))
|
return sorted(journal_entries + payment_entries, key=lambda k: k[2] or getdate(nowdate()))
|
@ -184,7 +184,7 @@ class GrossProfitGenerator(object):
|
|||||||
def get_returned_invoice_items(self):
|
def get_returned_invoice_items(self):
|
||||||
returned_invoices = frappe.db.sql("""
|
returned_invoices = frappe.db.sql("""
|
||||||
select
|
select
|
||||||
si.name, si_item.item_code, si_item.qty, si_item.base_amount, si.return_against
|
si.name, si_item.item_code, si_item.stock_qty as qty, si_item.base_net_amount as base_amount, si.return_against
|
||||||
from
|
from
|
||||||
`tabSales Invoice` si, `tabSales Invoice Item` si_item
|
`tabSales Invoice` si, `tabSales Invoice Item` si_item
|
||||||
where
|
where
|
||||||
@ -200,7 +200,7 @@ class GrossProfitGenerator(object):
|
|||||||
|
|
||||||
def skip_row(self, row, product_bundles):
|
def skip_row(self, row, product_bundles):
|
||||||
if self.filters.get("group_by") != "Invoice":
|
if self.filters.get("group_by") != "Invoice":
|
||||||
if not row.get(scrub(self.filters.get("group_by"))):
|
if not row.get(scrub(self.filters.get("group_by", ""))):
|
||||||
return True
|
return True
|
||||||
elif row.get("is_return") == 1:
|
elif row.get("is_return") == 1:
|
||||||
return True
|
return True
|
||||||
@ -316,7 +316,7 @@ class GrossProfitGenerator(object):
|
|||||||
on `tabSales Invoice Item`.parent = `tabSales Invoice`.name
|
on `tabSales Invoice Item`.parent = `tabSales Invoice`.name
|
||||||
{sales_team_table}
|
{sales_team_table}
|
||||||
where
|
where
|
||||||
`tabSales Invoice`.docstatus=1 {conditions} {match_cond}
|
`tabSales Invoice`.docstatus=1 and `tabSales Invoice`.is_opening!='Yes' {conditions} {match_cond}
|
||||||
order by
|
order by
|
||||||
`tabSales Invoice`.posting_date desc, `tabSales Invoice`.posting_time desc"""
|
`tabSales Invoice`.posting_date desc, `tabSales Invoice`.posting_time desc"""
|
||||||
.format(conditions=conditions, sales_person_cols=sales_person_cols,
|
.format(conditions=conditions, sales_person_cols=sales_person_cols,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"allow_copy": 0,
|
||||||
|
"allow_events_in_timeline": 0,
|
||||||
"allow_guest_to_view": 0,
|
"allow_guest_to_view": 0,
|
||||||
"allow_import": 1,
|
"allow_import": 1,
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
@ -154,7 +155,7 @@
|
|||||||
"columns": 0,
|
"columns": 0,
|
||||||
"fetch_from": "item_code.asset_category",
|
"fetch_from": "item_code.asset_category",
|
||||||
"fieldname": "asset_category",
|
"fieldname": "asset_category",
|
||||||
"fieldtype": "Read Only",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
"ignore_user_permissions": 0,
|
"ignore_user_permissions": 0,
|
||||||
"ignore_xss_filter": 0,
|
"ignore_xss_filter": 0,
|
||||||
@ -165,12 +166,12 @@
|
|||||||
"label": "Asset Category",
|
"label": "Asset Category",
|
||||||
"length": 0,
|
"length": 0,
|
||||||
"no_copy": 0,
|
"no_copy": 0,
|
||||||
"options": "",
|
"options": "Asset Category",
|
||||||
"permlevel": 0,
|
"permlevel": 0,
|
||||||
"precision": "",
|
"precision": "",
|
||||||
"print_hide": 0,
|
"print_hide": 0,
|
||||||
"print_hide_if_no_value": 0,
|
"print_hide_if_no_value": 0,
|
||||||
"read_only": 0,
|
"read_only": 1,
|
||||||
"remember_last_selected_value": 0,
|
"remember_last_selected_value": 0,
|
||||||
"report_hide": 0,
|
"report_hide": 0,
|
||||||
"reqd": 0,
|
"reqd": 0,
|
||||||
@ -1881,7 +1882,7 @@
|
|||||||
"issingle": 0,
|
"issingle": 0,
|
||||||
"istable": 0,
|
"istable": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"modified": "2018-08-21 14:44:24.507215",
|
"modified": "2019-01-15 16:12:48.314196",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Assets",
|
"module": "Assets",
|
||||||
"name": "Asset",
|
"name": "Asset",
|
||||||
|
@ -9,6 +9,7 @@ from frappe.utils import cstr, nowdate, getdate, flt, get_last_day, add_days, ad
|
|||||||
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries, scrap_asset, restore_asset
|
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries, scrap_asset, restore_asset
|
||||||
from erpnext.assets.doctype.asset.asset import make_sales_invoice, make_purchase_invoice
|
from erpnext.assets.doctype.asset.asset import make_sales_invoice, make_purchase_invoice
|
||||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||||
|
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice as make_invoice
|
||||||
|
|
||||||
class TestAsset(unittest.TestCase):
|
class TestAsset(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -494,6 +495,15 @@ class TestAsset(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(gle, expected_gle)
|
self.assertEqual(gle, expected_gle)
|
||||||
|
|
||||||
|
def test_expense_head(self):
|
||||||
|
pr = make_purchase_receipt(item_code="Macbook Pro",
|
||||||
|
qty=2, rate=200000.0, location="Test Location")
|
||||||
|
|
||||||
|
doc = make_invoice(pr.name)
|
||||||
|
|
||||||
|
self.assertEquals('Asset Received But Not Billed - _TC', doc.items[0].expense_account)
|
||||||
|
|
||||||
|
|
||||||
def create_asset_data():
|
def create_asset_data():
|
||||||
if not frappe.db.exists("Asset Category", "Computers"):
|
if not frappe.db.exists("Asset Category", "Computers"):
|
||||||
create_asset_category()
|
create_asset_category()
|
||||||
|
@ -604,12 +604,13 @@ class AccountsController(TransactionBase):
|
|||||||
advance.account_currency)
|
advance.account_currency)
|
||||||
|
|
||||||
if advance.account_currency == self.currency:
|
if advance.account_currency == self.currency:
|
||||||
order_total = self.grand_total
|
order_total = self.get("rounded_total") or self.grand_total
|
||||||
formatted_order_total = fmt_money(order_total, precision=self.precision("grand_total"),
|
precision = "rounded_total" if self.get("rounded_total") else "grand_total"
|
||||||
currency=advance.account_currency)
|
|
||||||
else:
|
else:
|
||||||
order_total = self.base_grand_total
|
order_total = self.get("base_rounded_total") or self.base_grand_total
|
||||||
formatted_order_total = fmt_money(order_total, precision=self.precision("base_grand_total"),
|
precision = "base_rounded_total" if self.get("base_rounded_total") else "base_grand_total"
|
||||||
|
|
||||||
|
formatted_order_total = fmt_money(order_total, precision=self.precision(precision),
|
||||||
currency=advance.account_currency)
|
currency=advance.account_currency)
|
||||||
|
|
||||||
if self.currency == self.company_currency and advance_paid > order_total:
|
if self.currency == self.company_currency and advance_paid > order_total:
|
||||||
|
@ -678,7 +678,9 @@ class BuyingController(StockController):
|
|||||||
frappe.db.sql("delete from `tabSerial No` where purchase_document_no=%s", self.name)
|
frappe.db.sql("delete from `tabSerial No` where purchase_document_no=%s", self.name)
|
||||||
|
|
||||||
def validate_schedule_date(self):
|
def validate_schedule_date(self):
|
||||||
if not self.schedule_date and self.get("items"):
|
if not self.get("items"):
|
||||||
|
return
|
||||||
|
if not self.schedule_date:
|
||||||
self.schedule_date = min([d.schedule_date for d in self.get("items")])
|
self.schedule_date = min([d.schedule_date for d in self.get("items")])
|
||||||
|
|
||||||
if self.schedule_date:
|
if self.schedule_date:
|
||||||
|
@ -14,6 +14,7 @@ from erpnext.stock import get_warehouse_account_map
|
|||||||
|
|
||||||
class QualityInspectionRequiredError(frappe.ValidationError): pass
|
class QualityInspectionRequiredError(frappe.ValidationError): pass
|
||||||
class QualityInspectionRejectedError(frappe.ValidationError): pass
|
class QualityInspectionRejectedError(frappe.ValidationError): pass
|
||||||
|
class QualityInspectionNotSubmittedError(frappe.ValidationError): pass
|
||||||
|
|
||||||
class StockController(AccountsController):
|
class StockController(AccountsController):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
@ -338,18 +339,20 @@ class StockController(AccountsController):
|
|||||||
qa_required = True
|
qa_required = True
|
||||||
elif self.doctype == "Stock Entry" and not d.quality_inspection and d.t_warehouse:
|
elif self.doctype == "Stock Entry" and not d.quality_inspection and d.t_warehouse:
|
||||||
qa_required = True
|
qa_required = True
|
||||||
|
if self.docstatus == 1 and d.quality_inspection:
|
||||||
if qa_required:
|
|
||||||
frappe.msgprint(_("Quality Inspection required for Item {0}").format(d.item_code))
|
|
||||||
if self.docstatus==1:
|
|
||||||
raise QualityInspectionRequiredError
|
|
||||||
elif self.docstatus == 1:
|
|
||||||
if d.quality_inspection:
|
|
||||||
qa_doc = frappe.get_doc("Quality Inspection", d.quality_inspection)
|
qa_doc = frappe.get_doc("Quality Inspection", d.quality_inspection)
|
||||||
|
if qa_doc.docstatus == 0:
|
||||||
|
link = frappe.utils.get_link_to_form('Quality Inspection', d.quality_inspection)
|
||||||
|
frappe.throw(_("Quality Inspection: {0} is not submitted for the item: {1} in row {2}").format(link, d.item_code, d.idx), QualityInspectionNotSubmittedError)
|
||||||
|
|
||||||
qa_failed = any([r.status=="Rejected" for r in qa_doc.readings])
|
qa_failed = any([r.status=="Rejected" for r in qa_doc.readings])
|
||||||
if qa_failed:
|
if qa_failed:
|
||||||
frappe.throw(_("Row {0}: Quality Inspection rejected for item {1}")
|
frappe.throw(_("Row {0}: Quality Inspection rejected for item {1}")
|
||||||
.format(d.idx, d.item_code), QualityInspectionRejectedError)
|
.format(d.idx, d.item_code), QualityInspectionRejectedError)
|
||||||
|
elif qa_required :
|
||||||
|
frappe.msgprint(_("Quality Inspection required for Item {0}").format(d.item_code))
|
||||||
|
if self.docstatus==1:
|
||||||
|
raise QualityInspectionRequiredError
|
||||||
|
|
||||||
|
|
||||||
def update_blanket_order(self):
|
def update_blanket_order(self):
|
||||||
|
@ -12,7 +12,7 @@ app_license = "GNU General Public License (v3)"
|
|||||||
source_link = "https://github.com/frappe/erpnext"
|
source_link = "https://github.com/frappe/erpnext"
|
||||||
|
|
||||||
develop_version = '12.x.x-develop'
|
develop_version = '12.x.x-develop'
|
||||||
staging_version = '11.0.3-beta.34'
|
staging_version = '11.0.3-beta.35'
|
||||||
|
|
||||||
error_report_email = "support@erpnext.com"
|
error_report_email = "support@erpnext.com"
|
||||||
|
|
||||||
@ -223,10 +223,15 @@ doc_events = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scheduler_events = {
|
scheduler_events = {
|
||||||
|
"all": [
|
||||||
|
"erpnext.projects.doctype.project.project.project_status_update_reminder"
|
||||||
|
],
|
||||||
"hourly": [
|
"hourly": [
|
||||||
'erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails',
|
'erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails',
|
||||||
"erpnext.accounts.doctype.subscription.subscription.process_all",
|
"erpnext.accounts.doctype.subscription.subscription.process_all",
|
||||||
"erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_settings.schedule_get_order_details"
|
"erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_settings.schedule_get_order_details",
|
||||||
|
"erpnext.projects.doctype.project.project.hourly_reminder",
|
||||||
|
"erpnext.projects.doctype.project.project.collect_project_status"
|
||||||
],
|
],
|
||||||
"daily": [
|
"daily": [
|
||||||
"erpnext.stock.reorder_item.reorder_item",
|
"erpnext.stock.reorder_item.reorder_item",
|
||||||
@ -245,7 +250,8 @@ scheduler_events = {
|
|||||||
"erpnext.assets.doctype.asset.asset.update_maintenance_status",
|
"erpnext.assets.doctype.asset.asset.update_maintenance_status",
|
||||||
"erpnext.assets.doctype.asset.asset.make_post_gl_entry",
|
"erpnext.assets.doctype.asset.asset.make_post_gl_entry",
|
||||||
"erpnext.crm.doctype.contract.contract.update_status_for_contracts",
|
"erpnext.crm.doctype.contract.contract.update_status_for_contracts",
|
||||||
"erpnext.projects.doctype.project.project.update_project_sales_billing"
|
"erpnext.projects.doctype.project.project.update_project_sales_billing",
|
||||||
|
"erpnext.projects.doctype.project.project.send_project_status_email_to_users"
|
||||||
],
|
],
|
||||||
"daily_long": [
|
"daily_long": [
|
||||||
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms"
|
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms"
|
||||||
|
@ -112,6 +112,10 @@ def get_user_emails_from_group(group):
|
|||||||
if isinstance(group_doc, string_types):
|
if isinstance(group_doc, string_types):
|
||||||
group_doc = frappe.get_doc('Daily Work Summary Group', group)
|
group_doc = frappe.get_doc('Daily Work Summary Group', group)
|
||||||
|
|
||||||
emails = [d.email for d in group_doc.users if frappe.db.get_value("User", d.user, "enabled")]
|
emails = get_users_email(group_doc)
|
||||||
|
|
||||||
return emails
|
return emails
|
||||||
|
|
||||||
|
def get_users_email(doc):
|
||||||
|
return [d.email for d in doc.users
|
||||||
|
if frappe.db.get_value("User", d.user, "enabled")]
|
||||||
|
@ -54,6 +54,9 @@ class EmployeeBoardingController(Document):
|
|||||||
where parenttype='User' and role=%s''', activity.role)
|
where parenttype='User' and role=%s''', activity.role)
|
||||||
users = users + user_list
|
users = users + user_list
|
||||||
|
|
||||||
|
if "Administrator" in users:
|
||||||
|
users.remove("Administrator")
|
||||||
|
|
||||||
# assign the task the users
|
# assign the task the users
|
||||||
if users:
|
if users:
|
||||||
self.assign_task_to_users(task, set(users))
|
self.assign_task_to_users(task, set(users))
|
||||||
|
@ -121,9 +121,11 @@ 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: false
|
||||||
},
|
},
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
|
refresh_field("items");
|
||||||
if(!r.exc) frm.refresh_fields();
|
if(!r.exc) frm.refresh_fields();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -8,6 +8,7 @@ from frappe import _
|
|||||||
from erpnext.setup.utils import get_exchange_rate
|
from erpnext.setup.utils import get_exchange_rate
|
||||||
from frappe.website.website_generator import WebsiteGenerator
|
from frappe.website.website_generator import WebsiteGenerator
|
||||||
from erpnext.stock.get_item_details import get_conversion_factor
|
from erpnext.stock.get_item_details import get_conversion_factor
|
||||||
|
from erpnext.stock.get_item_details import get_price_list_rate
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
|
|
||||||
@ -109,7 +110,11 @@ class BOM(WebsiteGenerator):
|
|||||||
"item_name": item.item_name,
|
"item_name": item.item_name,
|
||||||
"bom_no": item.bom_no,
|
"bom_no": item.bom_no,
|
||||||
"stock_qty": item.stock_qty,
|
"stock_qty": item.stock_qty,
|
||||||
"include_item_in_manufacturing": item.include_item_in_manufacturing
|
"include_item_in_manufacturing": item.include_item_in_manufacturing,
|
||||||
|
"qty": item.qty,
|
||||||
|
"uom": item.uom,
|
||||||
|
"stock_uom": item.stock_uom,
|
||||||
|
"conversion_factor": item.conversion_factor
|
||||||
})
|
})
|
||||||
for r in ret:
|
for r in ret:
|
||||||
if not item.get(r):
|
if not item.get(r):
|
||||||
@ -141,7 +146,7 @@ class BOM(WebsiteGenerator):
|
|||||||
'uom' : item and args['stock_uom'] or '',
|
'uom' : item and args['stock_uom'] or '',
|
||||||
'conversion_factor': 1,
|
'conversion_factor': 1,
|
||||||
'bom_no' : args['bom_no'],
|
'bom_no' : args['bom_no'],
|
||||||
'rate' : rate / self.conversion_rate if self.conversion_rate else rate,
|
'rate' : rate,
|
||||||
'qty' : args.get("qty") or args.get("stock_qty") or 1,
|
'qty' : args.get("qty") or args.get("stock_qty") or 1,
|
||||||
'stock_qty' : args.get("qty") or args.get("stock_qty") or 1,
|
'stock_qty' : args.get("qty") or args.get("stock_qty") or 1,
|
||||||
'base_rate' : rate,
|
'base_rate' : rate,
|
||||||
@ -173,34 +178,55 @@ class BOM(WebsiteGenerator):
|
|||||||
elif self.rm_cost_as_per == "Price List":
|
elif self.rm_cost_as_per == "Price List":
|
||||||
if not self.buying_price_list:
|
if not self.buying_price_list:
|
||||||
frappe.throw(_("Please select Price List"))
|
frappe.throw(_("Please select Price List"))
|
||||||
rate = frappe.db.get_value("Item Price", {"price_list": self.buying_price_list,
|
args = frappe._dict({
|
||||||
"item_code": arg["item_code"]}, "price_list_rate") or 0.0
|
"doctype": "BOM",
|
||||||
|
"price_list": self.buying_price_list,
|
||||||
price_list_currency = frappe.db.get_value("Price List",
|
"qty": arg.get("qty"),
|
||||||
self.buying_price_list, "currency")
|
"uom": arg.get("uom") or arg.get("stock_uom"),
|
||||||
if price_list_currency != self.company_currency():
|
"stock_uom": arg.get("stock_uom"),
|
||||||
rate = flt(rate * self.conversion_rate)
|
"transaction_type": "buying",
|
||||||
|
"company": self.company,
|
||||||
|
"currency": self.currency,
|
||||||
|
"conversion_rate": self.conversion_rate or 1,
|
||||||
|
"conversion_factor": arg.get("conversion_factor") or 1,
|
||||||
|
"plc_conversion_rate": 1
|
||||||
|
})
|
||||||
|
item_doc = frappe.get_doc("Item", arg.get("item_code"))
|
||||||
|
out = frappe._dict()
|
||||||
|
get_price_list_rate(args, item_doc, out)
|
||||||
|
rate = out.price_list_rate
|
||||||
|
|
||||||
if not rate:
|
if not rate:
|
||||||
frappe.msgprint(_("{0} not found for Item {1}")
|
if self.rm_cost_as_per == "Price List":
|
||||||
|
frappe.msgprint(_("Price not found for item {0} and price list {1}")
|
||||||
|
.format(arg["item_code"], self.buying_price_list), alert=True)
|
||||||
|
else:
|
||||||
|
frappe.msgprint(_("{0} not found for item {1}")
|
||||||
.format(self.rm_cost_as_per, arg["item_code"]), alert=True)
|
.format(self.rm_cost_as_per, arg["item_code"]), alert=True)
|
||||||
|
|
||||||
return flt(rate)
|
return flt(rate)
|
||||||
|
|
||||||
def update_cost(self, update_parent=True, from_child_bom=False):
|
def update_cost(self, update_parent=True, from_child_bom=False, save=True):
|
||||||
if self.docstatus == 2:
|
if self.docstatus == 2:
|
||||||
return
|
return
|
||||||
|
|
||||||
existing_bom_cost = self.total_cost
|
existing_bom_cost = self.total_cost
|
||||||
|
|
||||||
for d in self.get("items"):
|
for d in self.get("items"):
|
||||||
rate = self.get_rm_rate({"item_code": d.item_code, "bom_no": d.bom_no})
|
d.rate = self.get_rm_rate({
|
||||||
if rate:
|
"item_code": d.item_code,
|
||||||
d.rate = rate * flt(d.conversion_factor) / flt(self.conversion_rate)
|
"bom_no": d.bom_no,
|
||||||
|
"qty": d.qty,
|
||||||
|
"uom": d.uom,
|
||||||
|
"stock_uom": d.stock_uom,
|
||||||
|
"conversion_factor": d.conversion_factor
|
||||||
|
})
|
||||||
|
d.amount = flt(d.rate) * flt(d.qty)
|
||||||
|
|
||||||
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()
|
||||||
|
if save:
|
||||||
self.save()
|
self.save()
|
||||||
self.update_exploded_items()
|
self.update_exploded_items()
|
||||||
|
|
||||||
|
@ -97,6 +97,7 @@ class TestBOM(unittest.TestCase):
|
|||||||
self.assertEqual(bom.base_total_cost, 486000)
|
self.assertEqual(bom.base_total_cost, 486000)
|
||||||
|
|
||||||
def test_bom_cost_multi_uom_multi_currency(self):
|
def test_bom_cost_multi_uom_multi_currency(self):
|
||||||
|
frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependant", 1)
|
||||||
for item_code, rate in (("_Test Item", 3600), ("_Test Item Home Desktop Manufactured", 3000)):
|
for item_code, rate in (("_Test Item", 3600), ("_Test Item Home Desktop Manufactured", 3000)):
|
||||||
frappe.db.sql("delete from `tabItem Price` where price_list='_Test Price List' and item_code=%s",
|
frappe.db.sql("delete from `tabItem Price` where price_list='_Test Price List' and item_code=%s",
|
||||||
item_code)
|
item_code)
|
||||||
|
@ -1023,6 +1023,71 @@
|
|||||||
"set_only_once": 0,
|
"set_only_once": 0,
|
||||||
"translatable": 0,
|
"translatable": 0,
|
||||||
"unique": 0
|
"unique": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 0,
|
||||||
|
"columns": 0,
|
||||||
|
"fieldname": "operation",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"hidden": 0,
|
||||||
|
"ignore_user_permissions": 0,
|
||||||
|
"ignore_xss_filter": 0,
|
||||||
|
"in_filter": 0,
|
||||||
|
"in_global_search": 0,
|
||||||
|
"in_list_view": 0,
|
||||||
|
"in_standard_filter": 0,
|
||||||
|
"label": "Item operation",
|
||||||
|
"length": 0,
|
||||||
|
"no_copy": 0,
|
||||||
|
"options": "Operation",
|
||||||
|
"permlevel": 0,
|
||||||
|
"precision": "",
|
||||||
|
"print_hide": 0,
|
||||||
|
"print_hide_if_no_value": 0,
|
||||||
|
"read_only": 0,
|
||||||
|
"remember_last_selected_value": 0,
|
||||||
|
"report_hide": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"search_index": 0,
|
||||||
|
"set_only_once": 0,
|
||||||
|
"translatable": 0,
|
||||||
|
"unique": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 0,
|
||||||
|
"columns": 0,
|
||||||
|
"fieldname": "allow_alternative_item",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"hidden": 1,
|
||||||
|
"ignore_user_permissions": 0,
|
||||||
|
"ignore_xss_filter": 0,
|
||||||
|
"in_filter": 0,
|
||||||
|
"in_global_search": 0,
|
||||||
|
"in_list_view": 0,
|
||||||
|
"in_standard_filter": 0,
|
||||||
|
"label": "Allow Alternative Item",
|
||||||
|
"length": 0,
|
||||||
|
"no_copy": 0,
|
||||||
|
"permlevel": 0,
|
||||||
|
"precision": "",
|
||||||
|
"print_hide": 0,
|
||||||
|
"print_hide_if_no_value": 0,
|
||||||
|
"read_only": 1,
|
||||||
|
"remember_last_selected_value": 0,
|
||||||
|
"report_hide": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"search_index": 0,
|
||||||
|
"set_only_once": 0,
|
||||||
|
"translatable": 0,
|
||||||
|
"unique": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
"has_web_view": 0,
|
||||||
@ -1035,7 +1100,7 @@
|
|||||||
"issingle": 0,
|
"issingle": 0,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"modified": "2018-12-28 16:38:56.529079",
|
"modified": "2018-11-23 15:05:55.187136",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "BOM Item",
|
"name": "BOM Item",
|
||||||
|
@ -7,9 +7,9 @@ import frappe, json
|
|||||||
from frappe import msgprint, _
|
from frappe import msgprint, _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
|
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
|
||||||
from frappe.utils import cstr, flt, cint, nowdate, add_days, comma_and, now_datetime
|
from frappe.utils import cstr, flt, cint, nowdate, add_days, comma_and, now_datetime, ceil
|
||||||
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
|
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
|
||||||
from six import string_types
|
from six import string_types, iteritems
|
||||||
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
|
||||||
|
|
||||||
class ProductionPlan(Document):
|
class ProductionPlan(Document):
|
||||||
@ -372,40 +372,46 @@ class ProductionPlan(Document):
|
|||||||
else :
|
else :
|
||||||
msgprint(_("No material request created"))
|
msgprint(_("No material request created"))
|
||||||
|
|
||||||
def get_exploded_items(bom_wise_item_details, company, bom_no, include_non_stock_items):
|
def get_exploded_items(item_details, company, bom_no, include_non_stock_items, planned_qty=1):
|
||||||
for d in frappe.db.sql("""select bei.item_code, item.default_bom as bom,
|
for d in frappe.db.sql("""select bei.item_code, item.default_bom as bom,
|
||||||
ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0) as qty, item.item_name,
|
ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0)*%s as qty, item.item_name,
|
||||||
bei.description, bei.stock_uom, item.min_order_qty, bei.source_warehouse,
|
bei.description, bei.stock_uom, item.min_order_qty, bei.source_warehouse,
|
||||||
item.default_material_request_type, item.min_order_qty, item_default.default_warehouse
|
item.default_material_request_type, item.min_order_qty, item_default.default_warehouse,
|
||||||
|
item.purchase_uom, item_uom.conversion_factor
|
||||||
from
|
from
|
||||||
`tabBOM Explosion Item` bei
|
`tabBOM Explosion Item` bei
|
||||||
JOIN `tabBOM` bom ON bom.name = bei.parent
|
JOIN `tabBOM` bom ON bom.name = bei.parent
|
||||||
JOIN `tabItem` item ON item.name = bei.item_code
|
JOIN `tabItem` item ON item.name = bei.item_code
|
||||||
LEFT JOIN `tabItem Default` item_default
|
LEFT JOIN `tabItem Default` item_default
|
||||||
ON item_default.parent = item.name and item_default.company=%s
|
ON item_default.parent = item.name and item_default.company=%s
|
||||||
|
LEFT JOIN `tabUOM Conversion Detail` item_uom
|
||||||
|
ON item.name = item_uom.parent and item_uom.uom = item.purchase_uom
|
||||||
where
|
where
|
||||||
bei.docstatus < 2
|
bei.docstatus < 2
|
||||||
and bom.name=%s and item.is_stock_item in (1, {0})
|
and bom.name=%s and item.is_stock_item in (1, {0})
|
||||||
group by bei.item_code, bei.stock_uom""".format(0 if include_non_stock_items else 1),
|
group by bei.item_code, bei.stock_uom""".format(0 if include_non_stock_items else 1),
|
||||||
(company, bom_no), as_dict=1):
|
(planned_qty, company, bom_no), as_dict=1):
|
||||||
bom_wise_item_details.setdefault(d.get('item_code'), d)
|
item_details.setdefault(d.get('item_code'), d)
|
||||||
return bom_wise_item_details
|
return item_details
|
||||||
|
|
||||||
def get_subitems(doc, data, bom_wise_item_details, bom_no, company, include_non_stock_items, include_subcontracted_items, parent_qty):
|
def get_subitems(doc, data, item_details, bom_no, company, include_non_stock_items,
|
||||||
|
include_subcontracted_items, parent_qty, planned_qty=1):
|
||||||
items = frappe.db.sql("""
|
items = frappe.db.sql("""
|
||||||
SELECT
|
SELECT
|
||||||
bom_item.item_code, default_material_request_type, item.item_name,
|
bom_item.item_code, default_material_request_type, item.item_name,
|
||||||
ifnull(%(parent_qty)s * sum(bom_item.stock_qty/ifnull(bom.quantity, 1)), 0) as qty,
|
ifnull(%(parent_qty)s * sum(bom_item.stock_qty/ifnull(bom.quantity, 1)) * %(planned_qty)s, 0) as qty,
|
||||||
item.is_sub_contracted_item as is_sub_contracted, bom_item.source_warehouse,
|
item.is_sub_contracted_item as is_sub_contracted, bom_item.source_warehouse,
|
||||||
item.default_bom as default_bom, bom_item.description as description,
|
item.default_bom as default_bom, bom_item.description as description,
|
||||||
bom_item.stock_uom as stock_uom, item.min_order_qty as min_order_qty,
|
bom_item.stock_uom as stock_uom, item.min_order_qty as min_order_qty,
|
||||||
item_default.default_warehouse
|
item_default.default_warehouse, item.purchase_uom, item_uom.conversion_factor
|
||||||
FROM
|
FROM
|
||||||
`tabBOM Item` bom_item
|
`tabBOM Item` bom_item
|
||||||
JOIN `tabBOM` bom ON bom.name = bom_item.parent
|
JOIN `tabBOM` bom ON bom.name = bom_item.parent
|
||||||
JOIN tabItem item ON bom_item.item_code = item.name
|
JOIN tabItem item ON bom_item.item_code = item.name
|
||||||
LEFT JOIN `tabItem Default` item_default
|
LEFT JOIN `tabItem Default` item_default
|
||||||
ON item.name = item_default.parent and item_default.company = %(company)s
|
ON item.name = item_default.parent and item_default.company = %(company)s
|
||||||
|
LEFT JOIN `tabUOM Conversion Detail` item_uom
|
||||||
|
ON item.name = item_uom.parent and item_uom.uom = item.purchase_uom
|
||||||
where
|
where
|
||||||
bom.name = %(bom)s
|
bom.name = %(bom)s
|
||||||
and bom_item.docstatus < 2
|
and bom_item.docstatus < 2
|
||||||
@ -413,45 +419,61 @@ def get_subitems(doc, data, bom_wise_item_details, bom_no, company, include_non_
|
|||||||
group by bom_item.item_code""".format(0 if include_non_stock_items else 1),{
|
group by bom_item.item_code""".format(0 if include_non_stock_items else 1),{
|
||||||
'bom': bom_no,
|
'bom': bom_no,
|
||||||
'parent_qty': parent_qty,
|
'parent_qty': parent_qty,
|
||||||
|
'planned_qty': planned_qty,
|
||||||
'company': company
|
'company': company
|
||||||
}, as_dict=1)
|
}, as_dict=1)
|
||||||
|
|
||||||
for d in items:
|
for d in items:
|
||||||
if not data.get('include_exploded_items') or not d.default_bom:
|
if not data.get('include_exploded_items') or not d.default_bom:
|
||||||
if d.item_code in bom_wise_item_details:
|
if d.item_code in item_details:
|
||||||
bom_wise_item_details[d.item_code].qty = bom_wise_item_details[d.item_code].qty + d.qty
|
item_details[d.item_code].qty = item_details[d.item_code].qty + d.qty
|
||||||
else:
|
else:
|
||||||
bom_wise_item_details[d.item_code] = d
|
item_details[d.item_code] = d
|
||||||
|
|
||||||
if data.get('include_exploded_items') and d.default_bom:
|
if data.get('include_exploded_items') and d.default_bom:
|
||||||
if ((d.default_material_request_type in ["Manufacture", "Purchase"] and
|
if ((d.default_material_request_type in ["Manufacture", "Purchase"] and
|
||||||
not d.is_sub_contracted) or (d.is_sub_contracted and include_subcontracted_items)):
|
not d.is_sub_contracted) or (d.is_sub_contracted and include_subcontracted_items)):
|
||||||
if d.qty > 0:
|
if d.qty > 0:
|
||||||
get_subitems(doc, data, bom_wise_item_details, d.default_bom, company, include_non_stock_items, include_subcontracted_items, d.qty)
|
get_subitems(doc, data, item_details, d.default_bom, company,
|
||||||
return bom_wise_item_details
|
include_non_stock_items, include_subcontracted_items, d.qty)
|
||||||
|
return item_details
|
||||||
|
|
||||||
def add_item_in_material_request_items(doc, planned_qty, ignore_existing_ordered_qty, item, row, data, warehouse, company):
|
def get_material_request_items(row, sales_order, company, ignore_existing_ordered_qty, warehouse):
|
||||||
total_qty = row.qty * planned_qty
|
total_qty = row['qty']
|
||||||
projected_qty, actual_qty = get_bin_details(row)
|
projected_qty, actual_qty = get_bin_details(row)
|
||||||
|
|
||||||
requested_qty = 0
|
requested_qty = 0
|
||||||
if ignore_existing_ordered_qty:
|
if ignore_existing_ordered_qty:
|
||||||
requested_qty = total_qty
|
requested_qty = total_qty
|
||||||
else:
|
elif total_qty > projected_qty:
|
||||||
requested_qty = total_qty - projected_qty
|
requested_qty = total_qty - projected_qty
|
||||||
if requested_qty > 0 and requested_qty < row.min_order_qty:
|
if requested_qty > 0 and requested_qty < row['min_order_qty']:
|
||||||
requested_qty = row.min_order_qty
|
requested_qty = row['min_order_qty']
|
||||||
item_group_defaults = get_item_group_defaults(item, company)
|
item_group_defaults = get_item_group_defaults(row.item_code, company)
|
||||||
|
|
||||||
|
if not row['purchase_uom']:
|
||||||
|
row['purchase_uom'] = row['stock_uom']
|
||||||
|
|
||||||
|
if row['purchase_uom'] != row['stock_uom']:
|
||||||
|
if not row['conversion_factor']:
|
||||||
|
frappe.throw(_("UOM Conversion factor ({0} -> {1}) not found for item: {2}")
|
||||||
|
.format(row['purchase_uom'], row['stock_uom'], row.item_code))
|
||||||
|
requested_qty = requested_qty / row['conversion_factor']
|
||||||
|
|
||||||
|
if frappe.db.get_value("UOM", row['purchase_uom'], "must_be_whole_number"):
|
||||||
|
requested_qty = ceil(requested_qty)
|
||||||
|
|
||||||
if requested_qty > 0:
|
if requested_qty > 0:
|
||||||
doc.setdefault('mr_items', []).append({
|
return {
|
||||||
'item_code': item,
|
'item_code': row.item_code,
|
||||||
'item_name': row.item_name,
|
'item_name': row.item_name,
|
||||||
'quantity': requested_qty,
|
'quantity': requested_qty,
|
||||||
'warehouse': warehouse or row.source_warehouse or row.default_warehouse or item_group_defaults.get("default_warehouse"),
|
'warehouse': warehouse or row.get('source_warehouse') \
|
||||||
|
or row.get('default_warehouse') or item_group_defaults.get("default_warehouse"),
|
||||||
'actual_qty': actual_qty,
|
'actual_qty': actual_qty,
|
||||||
'min_order_qty': row.min_order_qty,
|
'min_order_qty': row['min_order_qty'],
|
||||||
'sales_order': data.get('sales_order')
|
'sales_order': sales_order
|
||||||
})
|
}
|
||||||
|
|
||||||
def get_sales_orders(self):
|
def get_sales_orders(self):
|
||||||
so_filter = item_filter = ""
|
so_filter = item_filter = ""
|
||||||
@ -487,8 +509,8 @@ def get_sales_orders(self):
|
|||||||
"project": self.project,
|
"project": self.project,
|
||||||
"item": self.item_code,
|
"item": self.item_code,
|
||||||
"company": self.company
|
"company": self.company
|
||||||
}, as_dict=1)
|
|
||||||
|
|
||||||
|
}, as_dict=1)
|
||||||
return open_so
|
return open_so
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@ -497,56 +519,96 @@ def get_bin_details(row):
|
|||||||
row = frappe._dict(json.loads(row))
|
row = frappe._dict(json.loads(row))
|
||||||
|
|
||||||
conditions = ""
|
conditions = ""
|
||||||
warehouse = row.source_warehouse or row.default_warehouse or row.warehouse
|
warehouse = row.get('source_warehouse') or row.get('default_warehouse')
|
||||||
if warehouse:
|
if warehouse:
|
||||||
conditions = " and warehouse='{0}'".format(frappe.db.escape(warehouse))
|
lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"])
|
||||||
|
conditions = " and exists(select name from `tabWarehouse` where lft >= {0} and rgt <= {1} and name=`tabBin`.warehouse)".format(lft, rgt)
|
||||||
|
|
||||||
item_projected_qty = frappe.db.sql(""" select ifnull(sum(projected_qty),0) as projected_qty,
|
item_projected_qty = frappe.db.sql(""" select ifnull(sum(projected_qty),0) as projected_qty,
|
||||||
ifnull(sum(actual_qty),0) as actual_qty from `tabBin`
|
ifnull(sum(actual_qty),0) as actual_qty from `tabBin`
|
||||||
where item_code = %(item_code)s {conditions}
|
where item_code = %(item_code)s {conditions}
|
||||||
""".format(conditions=conditions), { "item_code": row.item_code }, as_list=1)
|
""".format(conditions=conditions), { "item_code": row['item_code'] }, as_list=1)
|
||||||
|
|
||||||
return item_projected_qty and item_projected_qty[0] or (0,0)
|
return item_projected_qty and item_projected_qty[0] or (0,0)
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_items_for_material_requests(doc, company=None):
|
def get_items_for_material_requests(doc, sales_order=None, company=None):
|
||||||
if isinstance(doc, string_types):
|
if isinstance(doc, string_types):
|
||||||
doc = frappe._dict(json.loads(doc))
|
doc = frappe._dict(json.loads(doc))
|
||||||
|
|
||||||
doc['mr_items'] = []
|
doc['mr_items'] = []
|
||||||
po_items = doc.get('po_items') if doc.get('po_items') else doc.get('items')
|
po_items = doc.get('po_items') if doc.get('po_items') else doc.get('items')
|
||||||
|
company = doc.get('company')
|
||||||
|
|
||||||
|
so_item_details = frappe._dict()
|
||||||
for data in po_items:
|
for data in po_items:
|
||||||
warehouse = None
|
|
||||||
bom_wise_item_details = {}
|
|
||||||
|
|
||||||
if data.get('required_qty'):
|
|
||||||
planned_qty = data.get('required_qty')
|
|
||||||
bom_no = data.get('bom')
|
|
||||||
ignore_existing_ordered_qty = data.get('ignore_existing_ordered_qty')
|
|
||||||
include_non_stock_items = 1
|
|
||||||
warehouse = data.get('for_warehouse')
|
warehouse = data.get('for_warehouse')
|
||||||
if data.get('include_exploded_items'):
|
ignore_existing_ordered_qty = data.get('ignore_existing_ordered_qty') or doc.get('ignore_existing_ordered_qty')
|
||||||
include_subcontracted_items = 1
|
planned_qty = data.get('required_qty') or data.get('planned_qty')
|
||||||
|
item_details = {}
|
||||||
|
if data.get("bom") or data.get("bom_no"):
|
||||||
|
if data.get('required_qty'):
|
||||||
|
bom_no = data.get('bom')
|
||||||
|
include_non_stock_items = 1
|
||||||
|
include_subcontracted_items = 1 if data.get('include_exploded_items') else 0
|
||||||
else:
|
else:
|
||||||
include_subcontracted_items = 0
|
|
||||||
else:
|
|
||||||
planned_qty = data.get('planned_qty')
|
|
||||||
bom_no = data.get('bom_no')
|
bom_no = data.get('bom_no')
|
||||||
include_subcontracted_items = doc.get('include_subcontracted_items')
|
include_subcontracted_items = doc.get('include_subcontracted_items')
|
||||||
company = doc.get('company')
|
|
||||||
include_non_stock_items = doc.get('include_non_stock_items')
|
include_non_stock_items = doc.get('include_non_stock_items')
|
||||||
ignore_existing_ordered_qty = doc.get('ignore_existing_ordered_qty')
|
|
||||||
if not planned_qty:
|
if not planned_qty:
|
||||||
frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get('idx')))
|
frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get('idx')))
|
||||||
|
|
||||||
if data.get('include_exploded_items') and bom_no and include_subcontracted_items:
|
if bom_no:
|
||||||
|
if data.get('include_exploded_items') and include_subcontracted_items:
|
||||||
# fetch exploded items from BOM
|
# fetch exploded items from BOM
|
||||||
bom_wise_item_details = get_exploded_items(bom_wise_item_details, company, bom_no, include_non_stock_items)
|
item_details = get_exploded_items(item_details,
|
||||||
|
company, bom_no, include_non_stock_items, planned_qty=planned_qty)
|
||||||
else:
|
else:
|
||||||
bom_wise_item_details = get_subitems(doc, data, bom_wise_item_details, bom_no, company, include_non_stock_items, include_subcontracted_items, 1)
|
item_details = get_subitems(doc, data, item_details, bom_no, company,
|
||||||
for item, item_details in bom_wise_item_details.items():
|
include_non_stock_items, include_subcontracted_items, 1, planned_qty=planned_qty)
|
||||||
if item_details.qty > 0:
|
else:
|
||||||
add_item_in_material_request_items(doc, planned_qty, ignore_existing_ordered_qty, item, item_details, data, warehouse, company)
|
item_master = frappe.get_doc('Item', data['item_code']).as_dict()
|
||||||
|
purchase_uom = item_master.purchase_uom or item_master.stock_uom
|
||||||
|
conversion_factor = 0
|
||||||
|
for d in item_master.get("uoms"):
|
||||||
|
if d.uom == purchase_uom:
|
||||||
|
conversion_factor = d.conversion_factor
|
||||||
|
|
||||||
return doc['mr_items']
|
item_details[item_master.name] = frappe._dict(
|
||||||
|
{
|
||||||
|
'item_name' : item_master.item_name,
|
||||||
|
'default_bom' : doc.bom,
|
||||||
|
'purchase_uom' : purchase_uom,
|
||||||
|
'default_warehouse': item_master.default_warehouse,
|
||||||
|
'min_order_qty' : item_master.min_order_qty,
|
||||||
|
'default_material_request_type' : item_master.default_material_request_type,
|
||||||
|
'qty': planned_qty or 1,
|
||||||
|
'is_sub_contracted' : item_master.is_subcontracted_item,
|
||||||
|
'item_code' : item_master.name,
|
||||||
|
'description' : item_master.description,
|
||||||
|
'stock_uom' : item_master.stock_uom,
|
||||||
|
'conversion_factor' : conversion_factor,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if not sales_order:
|
||||||
|
sales_order = doc.get("sales_order")
|
||||||
|
|
||||||
|
for item_code, details in iteritems(item_details):
|
||||||
|
so_item_details.setdefault(sales_order, frappe._dict())
|
||||||
|
if item_code in so_item_details.get(sales_order, {}):
|
||||||
|
so_item_details[sales_order][item_code]['qty'] = so_item_details[sales_order][item_code].get("qty", 0) + flt(details.qty)
|
||||||
|
else:
|
||||||
|
so_item_details[sales_order][item_code] = details
|
||||||
|
|
||||||
|
mr_items = []
|
||||||
|
for sales_order, item_code in iteritems(so_item_details):
|
||||||
|
item_dict = so_item_details[sales_order]
|
||||||
|
for details in item_dict.values():
|
||||||
|
if details.qty > 0:
|
||||||
|
items = get_material_request_items(details, sales_order, company,
|
||||||
|
ignore_existing_ordered_qty, warehouse)
|
||||||
|
if items:
|
||||||
|
mr_items.append(items)
|
||||||
|
|
||||||
|
return mr_items
|
||||||
|
@ -1,552 +0,0 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
|
||||||
# License: GNU General Public License v3. See license.txt
|
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
import frappe
|
|
||||||
from frappe.utils import cstr, flt, cint, nowdate, add_days, comma_and
|
|
||||||
|
|
||||||
from frappe import msgprint, _
|
|
||||||
|
|
||||||
from frappe.model.document import Document
|
|
||||||
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
|
|
||||||
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
|
|
||||||
|
|
||||||
class ProductionPlanningTool(Document):
|
|
||||||
def clear_table(self, table_name):
|
|
||||||
self.set(table_name, [])
|
|
||||||
|
|
||||||
def validate_company(self):
|
|
||||||
if not self.company:
|
|
||||||
frappe.throw(_("Please enter Company"))
|
|
||||||
|
|
||||||
def get_open_sales_orders(self):
|
|
||||||
""" Pull sales orders which are pending to deliver based on criteria selected"""
|
|
||||||
so_filter = item_filter = ""
|
|
||||||
if self.from_date:
|
|
||||||
so_filter += " and so.transaction_date >= %(from_date)s"
|
|
||||||
if self.to_date:
|
|
||||||
so_filter += " and so.transaction_date <= %(to_date)s"
|
|
||||||
if self.customer:
|
|
||||||
so_filter += " and so.customer = %(customer)s"
|
|
||||||
if self.project:
|
|
||||||
so_filter += " and so.project = %(project)s"
|
|
||||||
|
|
||||||
if self.fg_item:
|
|
||||||
item_filter += " and so_item.item_code = %(item)s"
|
|
||||||
|
|
||||||
open_so = frappe.db.sql("""
|
|
||||||
select distinct so.name, so.transaction_date, so.customer, so.base_grand_total
|
|
||||||
from `tabSales Order` so, `tabSales Order Item` so_item
|
|
||||||
where so_item.parent = so.name
|
|
||||||
and so.docstatus = 1 and so.status not in ("Stopped", "Closed")
|
|
||||||
and so.company = %(company)s
|
|
||||||
and so_item.qty > so_item.delivered_qty {0} {1}
|
|
||||||
and (exists (select name from `tabBOM` bom where bom.item=so_item.item_code
|
|
||||||
and bom.is_active = 1)
|
|
||||||
or exists (select name from `tabPacked Item` pi
|
|
||||||
where pi.parent = so.name and pi.parent_item = so_item.item_code
|
|
||||||
and exists (select name from `tabBOM` bom where bom.item=pi.item_code
|
|
||||||
and bom.is_active = 1)))
|
|
||||||
""".format(so_filter, item_filter), {
|
|
||||||
"from_date": self.from_date,
|
|
||||||
"to_date": self.to_date,
|
|
||||||
"customer": self.customer,
|
|
||||||
"project": self.project,
|
|
||||||
"item": self.fg_item,
|
|
||||||
"company": self.company
|
|
||||||
}, as_dict=1)
|
|
||||||
|
|
||||||
self.add_so_in_table(open_so)
|
|
||||||
|
|
||||||
def add_so_in_table(self, open_so):
|
|
||||||
""" Add sales orders in the table"""
|
|
||||||
self.clear_table("sales_orders")
|
|
||||||
|
|
||||||
so_list = []
|
|
||||||
for r in open_so:
|
|
||||||
if cstr(r['name']) not in so_list:
|
|
||||||
pp_so = self.append('sales_orders', {})
|
|
||||||
pp_so.sales_order = r['name']
|
|
||||||
pp_so.sales_order_date = cstr(r['transaction_date'])
|
|
||||||
pp_so.customer = cstr(r['customer'])
|
|
||||||
pp_so.grand_total = flt(r['base_grand_total'])
|
|
||||||
|
|
||||||
def get_pending_material_requests(self):
|
|
||||||
""" Pull Material Requests that are pending based on criteria selected"""
|
|
||||||
mr_filter = item_filter = ""
|
|
||||||
if self.from_date:
|
|
||||||
mr_filter += " and mr.transaction_date >= %(from_date)s"
|
|
||||||
if self.to_date:
|
|
||||||
mr_filter += " and mr.transaction_date <= %(to_date)s"
|
|
||||||
if self.warehouse:
|
|
||||||
mr_filter += " and mr_item.warehouse = %(warehouse)s"
|
|
||||||
|
|
||||||
if self.fg_item:
|
|
||||||
item_filter += " and mr_item.item_code = %(item)s"
|
|
||||||
|
|
||||||
pending_mr = frappe.db.sql("""
|
|
||||||
select distinct mr.name, mr.transaction_date
|
|
||||||
from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item
|
|
||||||
where mr_item.parent = mr.name
|
|
||||||
and mr.material_request_type = "Manufacture"
|
|
||||||
and mr.docstatus = 1
|
|
||||||
and mr_item.qty > ifnull(mr_item.ordered_qty,0) {0} {1}
|
|
||||||
and (exists (select name from `tabBOM` bom where bom.item=mr_item.item_code
|
|
||||||
and bom.is_active = 1))
|
|
||||||
""".format(mr_filter, item_filter), {
|
|
||||||
"from_date": self.from_date,
|
|
||||||
"to_date": self.to_date,
|
|
||||||
"warehouse": self.warehouse,
|
|
||||||
"item": self.fg_item
|
|
||||||
}, as_dict=1)
|
|
||||||
|
|
||||||
self.add_mr_in_table(pending_mr)
|
|
||||||
|
|
||||||
def add_mr_in_table(self, pending_mr):
|
|
||||||
""" Add Material Requests in the table"""
|
|
||||||
self.clear_table("material_requests")
|
|
||||||
|
|
||||||
mr_list = []
|
|
||||||
for r in pending_mr:
|
|
||||||
if cstr(r['name']) not in mr_list:
|
|
||||||
mr = self.append('material_requests', {})
|
|
||||||
mr.material_request = r['name']
|
|
||||||
mr.material_request_date = cstr(r['transaction_date'])
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
if self.get_items_from == "Sales Order":
|
|
||||||
self.get_so_items()
|
|
||||||
elif self.get_items_from == "Material Request":
|
|
||||||
self.get_mr_items()
|
|
||||||
|
|
||||||
def get_so_items(self):
|
|
||||||
so_list = [d.sales_order for d in self.get('sales_orders') if d.sales_order]
|
|
||||||
if not so_list:
|
|
||||||
msgprint(_("Please enter Sales Orders in the above table"))
|
|
||||||
return []
|
|
||||||
|
|
||||||
item_condition = ""
|
|
||||||
if self.fg_item:
|
|
||||||
item_condition = ' and so_item.item_code = "{0}"'.format(frappe.db.escape(self.fg_item))
|
|
||||||
|
|
||||||
items = frappe.db.sql("""select distinct parent, item_code, warehouse,
|
|
||||||
(qty - delivered_qty)*conversion_factor as pending_qty
|
|
||||||
from `tabSales Order Item` so_item
|
|
||||||
where parent in (%s) and docstatus = 1 and qty > delivered_qty
|
|
||||||
and exists (select name from `tabBOM` bom where bom.item=so_item.item_code
|
|
||||||
and bom.is_active = 1) %s""" % \
|
|
||||||
(", ".join(["%s"] * len(so_list)), item_condition), tuple(so_list), as_dict=1)
|
|
||||||
|
|
||||||
if self.fg_item:
|
|
||||||
item_condition = ' and pi.item_code = "{0}"'.format(frappe.db.escape(self.fg_item))
|
|
||||||
|
|
||||||
packed_items = frappe.db.sql("""select distinct pi.parent, pi.item_code, pi.warehouse as warehouse,
|
|
||||||
(((so_item.qty - so_item.delivered_qty) * pi.qty) / so_item.qty)
|
|
||||||
as pending_qty
|
|
||||||
from `tabSales Order Item` so_item, `tabPacked Item` pi
|
|
||||||
where so_item.parent = pi.parent and so_item.docstatus = 1
|
|
||||||
and pi.parent_item = so_item.item_code
|
|
||||||
and so_item.parent in (%s) and so_item.qty > so_item.delivered_qty
|
|
||||||
and exists (select name from `tabBOM` bom where bom.item=pi.item_code
|
|
||||||
and bom.is_active = 1) %s""" % \
|
|
||||||
(", ".join(["%s"] * len(so_list)), item_condition), tuple(so_list), as_dict=1)
|
|
||||||
|
|
||||||
self.add_items(items + packed_items)
|
|
||||||
|
|
||||||
def get_mr_items(self):
|
|
||||||
mr_list = [d.material_request for d in self.get('material_requests') if d.material_request]
|
|
||||||
if not mr_list:
|
|
||||||
msgprint(_("Please enter Material Requests in the above table"))
|
|
||||||
return []
|
|
||||||
|
|
||||||
item_condition = ""
|
|
||||||
if self.fg_item:
|
|
||||||
item_condition = ' and mr_item.item_code = "' + frappe.db.escape(self.fg_item, percent=False) + '"'
|
|
||||||
|
|
||||||
items = frappe.db.sql("""select distinct parent, name, item_code, warehouse,
|
|
||||||
(qty - ordered_qty) as pending_qty
|
|
||||||
from `tabMaterial Request Item` mr_item
|
|
||||||
where parent in (%s) and docstatus = 1 and qty > ordered_qty
|
|
||||||
and exists (select name from `tabBOM` bom where bom.item=mr_item.item_code
|
|
||||||
and bom.is_active = 1) %s""" % \
|
|
||||||
(", ".join(["%s"] * len(mr_list)), item_condition), tuple(mr_list), as_dict=1)
|
|
||||||
|
|
||||||
self.add_items(items)
|
|
||||||
|
|
||||||
|
|
||||||
def add_items(self, items):
|
|
||||||
self.clear_table("items")
|
|
||||||
for p in items:
|
|
||||||
item_details = get_item_details(p['item_code'])
|
|
||||||
pi = self.append('items', {})
|
|
||||||
pi.warehouse = p['warehouse']
|
|
||||||
pi.item_code = p['item_code']
|
|
||||||
pi.description = item_details and item_details.description or ''
|
|
||||||
pi.stock_uom = item_details and item_details.stock_uom or ''
|
|
||||||
pi.bom_no = item_details and item_details.bom_no or ''
|
|
||||||
pi.planned_qty = flt(p['pending_qty'])
|
|
||||||
pi.pending_qty = flt(p['pending_qty'])
|
|
||||||
|
|
||||||
if self.get_items_from == "Sales Order":
|
|
||||||
pi.sales_order = p['parent']
|
|
||||||
elif self.get_items_from == "Material Request":
|
|
||||||
pi.material_request = p['parent']
|
|
||||||
pi.material_request_item = p['name']
|
|
||||||
|
|
||||||
def validate_data(self):
|
|
||||||
self.validate_company()
|
|
||||||
for d in self.get('items'):
|
|
||||||
if not d.bom_no:
|
|
||||||
frappe.throw(_("Please select BOM for Item in Row {0}".format(d.idx)))
|
|
||||||
else:
|
|
||||||
validate_bom_no(d.item_code, d.bom_no)
|
|
||||||
|
|
||||||
if not flt(d.planned_qty):
|
|
||||||
frappe.throw(_("Please enter Planned Qty for Item {0} at row {1}").format(d.item_code, d.idx))
|
|
||||||
|
|
||||||
def raise_work_orders(self):
|
|
||||||
"""It will raise work order (Draft) for all distinct FG items"""
|
|
||||||
self.validate_data()
|
|
||||||
|
|
||||||
from erpnext.utilities.transaction_base import validate_uom_is_integer
|
|
||||||
validate_uom_is_integer(self, "stock_uom", "planned_qty")
|
|
||||||
|
|
||||||
items = self.get_production_items()
|
|
||||||
|
|
||||||
wo_list = []
|
|
||||||
frappe.flags.mute_messages = True
|
|
||||||
|
|
||||||
for key in items:
|
|
||||||
work_order = self.create_work_order(items[key])
|
|
||||||
if work_order:
|
|
||||||
wo_list.append(work_order)
|
|
||||||
|
|
||||||
frappe.flags.mute_messages = False
|
|
||||||
|
|
||||||
if wo_list:
|
|
||||||
wo_list = ["""<a href="#Form/Work Order/%s" target="_blank">%s</a>""" % \
|
|
||||||
(p, p) for p in wo_list]
|
|
||||||
msgprint(_("{0} created").format(comma_and(wo_list)))
|
|
||||||
else :
|
|
||||||
msgprint(_("No Work Orders created"))
|
|
||||||
|
|
||||||
def get_production_items(self):
|
|
||||||
item_dict = {}
|
|
||||||
for d in self.get("items"):
|
|
||||||
item_details= {
|
|
||||||
"production_item" : d.item_code,
|
|
||||||
"sales_order" : d.sales_order,
|
|
||||||
"material_request" : d.material_request,
|
|
||||||
"material_request_item" : d.material_request_item,
|
|
||||||
"bom_no" : d.bom_no,
|
|
||||||
"description" : d.description,
|
|
||||||
"stock_uom" : d.stock_uom,
|
|
||||||
"company" : self.company,
|
|
||||||
"wip_warehouse" : "",
|
|
||||||
"fg_warehouse" : d.warehouse,
|
|
||||||
"status" : "Draft",
|
|
||||||
"project" : frappe.db.get_value("Sales Order", d.sales_order, "project")
|
|
||||||
}
|
|
||||||
|
|
||||||
""" Club similar BOM and item for processing in case of Sales Orders """
|
|
||||||
if self.get_items_from == "Material Request":
|
|
||||||
item_details.update({
|
|
||||||
"qty": d.planned_qty
|
|
||||||
})
|
|
||||||
item_dict[(d.item_code, d.material_request_item, d.warehouse)] = item_details
|
|
||||||
|
|
||||||
else:
|
|
||||||
item_details.update({
|
|
||||||
"qty":flt(item_dict.get((d.item_code, d.sales_order, d.warehouse),{})
|
|
||||||
.get("qty")) + flt(d.planned_qty)
|
|
||||||
})
|
|
||||||
item_dict[(d.item_code, d.sales_order, d.warehouse)] = item_details
|
|
||||||
|
|
||||||
return item_dict
|
|
||||||
|
|
||||||
def create_work_order(self, item_dict):
|
|
||||||
"""Create work order. Called from Production Planning Tool"""
|
|
||||||
from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError, get_default_warehouse
|
|
||||||
warehouse = get_default_warehouse()
|
|
||||||
wo = frappe.new_doc("Work Order")
|
|
||||||
wo.update(item_dict)
|
|
||||||
wo.set_work_order_operations()
|
|
||||||
if warehouse:
|
|
||||||
wo.wip_warehouse = warehouse.get('wip_warehouse')
|
|
||||||
if not wo.fg_warehouse:
|
|
||||||
wo.fg_warehouse = warehouse.get('fg_warehouse')
|
|
||||||
|
|
||||||
try:
|
|
||||||
wo.insert()
|
|
||||||
return wo.name
|
|
||||||
except OverProductionError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_so_wise_planned_qty(self):
|
|
||||||
"""
|
|
||||||
bom_dict {
|
|
||||||
bom_no: ['sales_order', 'qty']
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
bom_dict = {}
|
|
||||||
for d in self.get("items"):
|
|
||||||
if self.get_items_from == "Material Request":
|
|
||||||
bom_dict.setdefault(d.bom_no, []).append([d.material_request_item, flt(d.planned_qty)])
|
|
||||||
else:
|
|
||||||
bom_dict.setdefault(d.bom_no, []).append([d.sales_order, flt(d.planned_qty)])
|
|
||||||
return bom_dict
|
|
||||||
|
|
||||||
def download_raw_materials(self):
|
|
||||||
""" Create csv data for required raw material to produce finished goods"""
|
|
||||||
self.validate_data()
|
|
||||||
bom_dict = self.get_so_wise_planned_qty()
|
|
||||||
self.get_raw_materials(bom_dict)
|
|
||||||
return self.get_csv()
|
|
||||||
|
|
||||||
def get_raw_materials(self, bom_dict,non_stock_item=0):
|
|
||||||
""" Get raw materials considering sub-assembly items
|
|
||||||
{
|
|
||||||
"item_code": [qty_required, description, stock_uom, min_order_qty]
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
item_list = []
|
|
||||||
precision = frappe.get_precision("BOM Item", "stock_qty")
|
|
||||||
|
|
||||||
for bom, so_wise_qty in bom_dict.items():
|
|
||||||
bom_wise_item_details = {}
|
|
||||||
if self.use_multi_level_bom and self.only_raw_materials and self.include_subcontracted:
|
|
||||||
# get all raw materials with sub assembly childs
|
|
||||||
# Did not use qty_consumed_per_unit in the query, as it leads to rounding loss
|
|
||||||
for d in frappe.db.sql("""select fb.item_code,
|
|
||||||
ifnull(sum(fb.stock_qty/ifnull(bom.quantity, 1)), 0) as qty,
|
|
||||||
fb.description, fb.stock_uom, item.min_order_qty
|
|
||||||
from `tabBOM Explosion Item` fb, `tabBOM` bom, `tabItem` item
|
|
||||||
where bom.name = fb.parent and item.name = fb.item_code
|
|
||||||
and (item.is_sub_contracted_item = 0 or ifnull(item.default_bom, "")="")
|
|
||||||
""" + ("and item.is_stock_item = 1","")[non_stock_item] + """
|
|
||||||
and fb.docstatus<2 and bom.name=%(bom)s
|
|
||||||
group by fb.item_code, fb.stock_uom""", {"bom":bom}, as_dict=1):
|
|
||||||
bom_wise_item_details.setdefault(d.item_code, d)
|
|
||||||
else:
|
|
||||||
# Get all raw materials considering SA items as raw materials,
|
|
||||||
# so no childs of SA items
|
|
||||||
bom_wise_item_details = self.get_subitems(bom_wise_item_details, bom,1, \
|
|
||||||
self.use_multi_level_bom,self.only_raw_materials, self.include_subcontracted,non_stock_item)
|
|
||||||
|
|
||||||
for item, item_details in bom_wise_item_details.items():
|
|
||||||
for so_qty in so_wise_qty:
|
|
||||||
item_list.append([item, flt(flt(item_details.qty) * so_qty[1], precision),
|
|
||||||
item_details.description, item_details.stock_uom, item_details.min_order_qty,
|
|
||||||
so_qty[0]])
|
|
||||||
|
|
||||||
self.make_items_dict(item_list)
|
|
||||||
|
|
||||||
def get_subitems(self,bom_wise_item_details, bom, parent_qty, include_sublevel, only_raw, supply_subs,non_stock_item=0):
|
|
||||||
items = frappe.db.sql("""
|
|
||||||
SELECT
|
|
||||||
bom_item.item_code,
|
|
||||||
default_material_request_type,
|
|
||||||
ifnull(%(parent_qty)s * sum(bom_item.stock_qty/ifnull(bom.quantity, 1)), 0) as qty,
|
|
||||||
item.is_sub_contracted_item as is_sub_contracted,
|
|
||||||
item.default_bom as default_bom,
|
|
||||||
bom_item.description as description,
|
|
||||||
bom_item.stock_uom as stock_uom,
|
|
||||||
item.min_order_qty as min_order_qty
|
|
||||||
FROM
|
|
||||||
`tabBOM Item` bom_item,
|
|
||||||
`tabBOM` bom,
|
|
||||||
tabItem item
|
|
||||||
where
|
|
||||||
bom.name = bom_item.parent
|
|
||||||
and bom.name = %(bom)s
|
|
||||||
and bom_item.docstatus < 2
|
|
||||||
and bom_item.item_code = item.name
|
|
||||||
""" + ("and item.is_stock_item = 1", "")[non_stock_item] + """
|
|
||||||
group by bom_item.item_code""", {"bom": bom, "parent_qty": parent_qty}, as_dict=1)
|
|
||||||
|
|
||||||
for d in items:
|
|
||||||
if ((d.default_material_request_type == "Purchase"
|
|
||||||
and not (d.is_sub_contracted and only_raw and include_sublevel))
|
|
||||||
or (d.default_material_request_type == "Manufacture" and not only_raw)):
|
|
||||||
|
|
||||||
if d.item_code in bom_wise_item_details:
|
|
||||||
bom_wise_item_details[d.item_code].qty = bom_wise_item_details[d.item_code].qty + d.qty
|
|
||||||
else:
|
|
||||||
bom_wise_item_details[d.item_code] = d
|
|
||||||
|
|
||||||
if include_sublevel and d.default_bom:
|
|
||||||
if ((d.default_material_request_type == "Purchase" and d.is_sub_contracted and supply_subs)
|
|
||||||
or (d.default_material_request_type == "Manufacture")):
|
|
||||||
|
|
||||||
my_qty = 0
|
|
||||||
projected_qty = self.get_item_projected_qty(d.item_code)
|
|
||||||
if self.create_material_requests_for_all_required_qty:
|
|
||||||
my_qty = d.qty
|
|
||||||
else:
|
|
||||||
total_required_qty = flt(bom_wise_item_details.get(d.item_code, frappe._dict()).qty)
|
|
||||||
if (total_required_qty - d.qty) < projected_qty:
|
|
||||||
my_qty = total_required_qty - projected_qty
|
|
||||||
else:
|
|
||||||
my_qty = d.qty
|
|
||||||
|
|
||||||
if my_qty > 0:
|
|
||||||
self.get_subitems(bom_wise_item_details,
|
|
||||||
d.default_bom, my_qty, include_sublevel, only_raw, supply_subs)
|
|
||||||
|
|
||||||
return bom_wise_item_details
|
|
||||||
|
|
||||||
def make_items_dict(self, item_list):
|
|
||||||
if not getattr(self, "item_dict", None):
|
|
||||||
self.item_dict = {}
|
|
||||||
|
|
||||||
for i in item_list:
|
|
||||||
self.item_dict.setdefault(i[0], []).append([flt(i[1]), i[2], i[3], i[4], i[5]])
|
|
||||||
|
|
||||||
def get_csv(self):
|
|
||||||
item_list = [['Item Code', 'Description', 'Stock UOM', 'Required Qty', 'Warehouse',
|
|
||||||
'Quantity Requested for Purchase', 'Ordered Qty', 'Actual Qty']]
|
|
||||||
for item in self.item_dict:
|
|
||||||
total_qty = sum([flt(d[0]) for d in self.item_dict[item]])
|
|
||||||
item_list.append([item, self.item_dict[item][0][1], self.item_dict[item][0][2], total_qty])
|
|
||||||
item_qty = frappe.db.sql("""select warehouse, indented_qty, ordered_qty, actual_qty
|
|
||||||
from `tabBin` where item_code = %s""", item, as_dict=1)
|
|
||||||
|
|
||||||
i_qty, o_qty, a_qty = 0, 0, 0
|
|
||||||
for w in item_qty:
|
|
||||||
i_qty, o_qty, a_qty = i_qty + flt(w.indented_qty), o_qty + \
|
|
||||||
flt(w.ordered_qty), a_qty + flt(w.actual_qty)
|
|
||||||
|
|
||||||
item_list.append(['', '', '', '', w.warehouse, flt(w.indented_qty),
|
|
||||||
flt(w.ordered_qty), flt(w.actual_qty)])
|
|
||||||
if item_qty:
|
|
||||||
item_list.append(['', '', '', '', 'Total', i_qty, o_qty, a_qty])
|
|
||||||
else:
|
|
||||||
item_list.append(['', '', '', '', 'Total', 0, 0, 0])
|
|
||||||
|
|
||||||
return item_list
|
|
||||||
|
|
||||||
def raise_material_requests(self):
|
|
||||||
"""
|
|
||||||
Raise Material Request if projected qty is less than qty required
|
|
||||||
Requested qty should be shortage qty considering minimum order qty
|
|
||||||
"""
|
|
||||||
self.validate_data()
|
|
||||||
if not self.purchase_request_for_warehouse:
|
|
||||||
frappe.throw(_("Please enter Warehouse for which Material Request will be raised"))
|
|
||||||
|
|
||||||
bom_dict = self.get_so_wise_planned_qty()
|
|
||||||
self.get_raw_materials(bom_dict,self.create_material_requests_non_stock_request)
|
|
||||||
|
|
||||||
if self.item_dict:
|
|
||||||
self.create_material_request()
|
|
||||||
|
|
||||||
def get_requested_items(self):
|
|
||||||
items_to_be_requested = frappe._dict()
|
|
||||||
|
|
||||||
if not self.create_material_requests_for_all_required_qty:
|
|
||||||
item_projected_qty = self.get_projected_qty()
|
|
||||||
|
|
||||||
for item, so_item_qty in self.item_dict.items():
|
|
||||||
total_qty = sum([flt(d[0]) for d in so_item_qty])
|
|
||||||
requested_qty = 0
|
|
||||||
|
|
||||||
if self.create_material_requests_for_all_required_qty:
|
|
||||||
requested_qty = total_qty
|
|
||||||
elif total_qty > item_projected_qty.get(item, 0):
|
|
||||||
# shortage
|
|
||||||
requested_qty = total_qty - flt(item_projected_qty.get(item))
|
|
||||||
# consider minimum order qty
|
|
||||||
|
|
||||||
if requested_qty and requested_qty < flt(so_item_qty[0][3]):
|
|
||||||
requested_qty = flt(so_item_qty[0][3])
|
|
||||||
|
|
||||||
# distribute requested qty SO wise
|
|
||||||
for item_details in so_item_qty:
|
|
||||||
if requested_qty:
|
|
||||||
sales_order = item_details[4] or "No Sales Order"
|
|
||||||
if self.get_items_from == "Material Request":
|
|
||||||
sales_order = "No Sales Order"
|
|
||||||
if requested_qty <= item_details[0]:
|
|
||||||
adjusted_qty = requested_qty
|
|
||||||
else:
|
|
||||||
adjusted_qty = item_details[0]
|
|
||||||
|
|
||||||
items_to_be_requested.setdefault(item, {}).setdefault(sales_order, 0)
|
|
||||||
items_to_be_requested[item][sales_order] += adjusted_qty
|
|
||||||
requested_qty -= adjusted_qty
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
# requested qty >= total so qty, due to minimum order qty
|
|
||||||
if requested_qty:
|
|
||||||
items_to_be_requested.setdefault(item, {}).setdefault("No Sales Order", 0)
|
|
||||||
items_to_be_requested[item]["No Sales Order"] += requested_qty
|
|
||||||
|
|
||||||
return items_to_be_requested
|
|
||||||
|
|
||||||
def get_item_projected_qty(self,item):
|
|
||||||
conditions = ""
|
|
||||||
if self.purchase_request_for_warehouse:
|
|
||||||
conditions = " and warehouse='{0}'".format(frappe.db.escape(self.purchase_request_for_warehouse))
|
|
||||||
|
|
||||||
item_projected_qty = frappe.db.sql("""
|
|
||||||
select ifnull(sum(projected_qty),0) as qty
|
|
||||||
from `tabBin`
|
|
||||||
where item_code = %(item_code)s {conditions}
|
|
||||||
""".format(conditions=conditions), { "item_code": item }, as_dict=1)
|
|
||||||
|
|
||||||
return item_projected_qty[0].qty
|
|
||||||
|
|
||||||
def get_projected_qty(self):
|
|
||||||
items = self.item_dict.keys()
|
|
||||||
item_projected_qty = frappe.db.sql("""select item_code, sum(projected_qty)
|
|
||||||
from `tabBin` where item_code in (%s) and warehouse=%s group by item_code""" %
|
|
||||||
(", ".join(["%s"]*len(items)), '%s'), tuple(items + [self.purchase_request_for_warehouse]))
|
|
||||||
|
|
||||||
return dict(item_projected_qty)
|
|
||||||
|
|
||||||
def create_material_request(self):
|
|
||||||
items_to_be_requested = self.get_requested_items()
|
|
||||||
|
|
||||||
material_request_list = []
|
|
||||||
if items_to_be_requested:
|
|
||||||
for item in items_to_be_requested:
|
|
||||||
item_wrapper = frappe.get_doc("Item", item)
|
|
||||||
material_request = frappe.new_doc("Material Request")
|
|
||||||
material_request.update({
|
|
||||||
"transaction_date": nowdate(),
|
|
||||||
"status": "Draft",
|
|
||||||
"company": self.company,
|
|
||||||
"requested_by": frappe.session.user,
|
|
||||||
"schedule_date": add_days(nowdate(), cint(item_wrapper.lead_time_days)),
|
|
||||||
})
|
|
||||||
material_request.update({"material_request_type": item_wrapper.default_material_request_type})
|
|
||||||
|
|
||||||
for sales_order, requested_qty in items_to_be_requested[item].items():
|
|
||||||
material_request.append("items", {
|
|
||||||
"doctype": "Material Request Item",
|
|
||||||
"__islocal": 1,
|
|
||||||
"item_code": item,
|
|
||||||
"item_name": item_wrapper.item_name,
|
|
||||||
"description": item_wrapper.description,
|
|
||||||
"uom": item_wrapper.stock_uom,
|
|
||||||
"item_group": item_wrapper.item_group,
|
|
||||||
"brand": item_wrapper.brand,
|
|
||||||
"qty": requested_qty,
|
|
||||||
"schedule_date": add_days(nowdate(), cint(item_wrapper.lead_time_days)),
|
|
||||||
"warehouse": self.purchase_request_for_warehouse,
|
|
||||||
"sales_order": sales_order if sales_order!="No Sales Order" else None,
|
|
||||||
"project": frappe.db.get_value("Sales Order", sales_order, "project") \
|
|
||||||
if sales_order!="No Sales Order" else None
|
|
||||||
})
|
|
||||||
|
|
||||||
material_request.flags.ignore_permissions = 1
|
|
||||||
material_request.submit()
|
|
||||||
material_request_list.append(material_request.name)
|
|
||||||
|
|
||||||
if material_request_list:
|
|
||||||
message = ["""<a href="#Form/Material Request/%s" target="_blank">%s</a>""" % \
|
|
||||||
(p, p) for p in material_request_list]
|
|
||||||
msgprint(_("Material Requests {0} created").format(comma_and(message)))
|
|
||||||
else:
|
|
||||||
msgprint(_("Nothing to request"))
|
|
@ -349,7 +349,8 @@ frappe.ui.form.on("Work Order", {
|
|||||||
before_submit: function(frm) {
|
before_submit: function(frm) {
|
||||||
frm.toggle_reqd(["fg_warehouse", "wip_warehouse"], true);
|
frm.toggle_reqd(["fg_warehouse", "wip_warehouse"], true);
|
||||||
frm.fields_dict.required_items.grid.toggle_reqd("source_warehouse", true);
|
frm.fields_dict.required_items.grid.toggle_reqd("source_warehouse", true);
|
||||||
frm.toggle_reqd("transfer_material_against", frm.doc.operations);
|
frm.toggle_reqd("transfer_material_against",
|
||||||
|
frm.doc.operations && frm.doc.operations.length > 0);
|
||||||
frm.fields_dict.operations.grid.toggle_reqd("workstation", frm.doc.operations);
|
frm.fields_dict.operations.grid.toggle_reqd("workstation", frm.doc.operations);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
execute:import unidecode # new requirement
|
execute:import unidecode # new requirement
|
||||||
erpnext.patches.v8_0.move_perpetual_inventory_setting
|
erpnext.patches.v8_0.move_perpetual_inventory_setting
|
||||||
|
erpnext.patches.v8_9.set_print_zero_amount_taxes
|
||||||
|
erpnext.patches.v11_0.rename_production_order_to_work_order
|
||||||
erpnext.patches.v11_0.refactor_naming_series
|
erpnext.patches.v11_0.refactor_naming_series
|
||||||
erpnext.patches.v11_0.refactor_autoname_naming
|
erpnext.patches.v11_0.refactor_autoname_naming
|
||||||
erpnext.patches.v10_0.rename_schools_to_education
|
erpnext.patches.v10_0.rename_schools_to_education
|
||||||
erpnext.patches.v11_0.rename_production_order_to_work_order
|
|
||||||
erpnext.patches.v4_0.validate_v3_patch
|
erpnext.patches.v4_0.validate_v3_patch
|
||||||
erpnext.patches.v4_0.fix_employee_user_id
|
erpnext.patches.v4_0.fix_employee_user_id
|
||||||
erpnext.patches.v4_0.remove_employee_role_if_no_employee
|
erpnext.patches.v4_0.remove_employee_role_if_no_employee
|
||||||
@ -442,7 +443,6 @@ erpnext.patches.v8_9.add_setup_progress_actions #08-09-2017 #26-09-2017 #22-11-2
|
|||||||
erpnext.patches.v8_9.rename_company_sales_target_field
|
erpnext.patches.v8_9.rename_company_sales_target_field
|
||||||
erpnext.patches.v8_8.set_bom_rate_as_per_uom
|
erpnext.patches.v8_8.set_bom_rate_as_per_uom
|
||||||
erpnext.patches.v8_8.add_new_fields_in_accounts_settings
|
erpnext.patches.v8_8.add_new_fields_in_accounts_settings
|
||||||
erpnext.patches.v8_9.set_print_zero_amount_taxes
|
|
||||||
erpnext.patches.v8_9.set_default_customer_group
|
erpnext.patches.v8_9.set_default_customer_group
|
||||||
erpnext.patches.v8_9.delete_gst_doctypes_for_outside_india_accounts
|
erpnext.patches.v8_9.delete_gst_doctypes_for_outside_india_accounts
|
||||||
erpnext.patches.v8_9.set_default_fields_in_variant_settings
|
erpnext.patches.v8_9.set_default_fields_in_variant_settings
|
||||||
@ -572,7 +572,7 @@ execute:frappe.delete_doc_if_exists("Page", "sales-analytics")
|
|||||||
execute:frappe.delete_doc_if_exists("Page", "purchase-analytics")
|
execute:frappe.delete_doc_if_exists("Page", "purchase-analytics")
|
||||||
execute:frappe.delete_doc_if_exists("Page", "stock-analytics")
|
execute:frappe.delete_doc_if_exists("Page", "stock-analytics")
|
||||||
execute:frappe.delete_doc_if_exists("Page", "production-analytics")
|
execute:frappe.delete_doc_if_exists("Page", "production-analytics")
|
||||||
erpnext.patches.v11_0.ewaybill_fields_gst_india #2018-11-13
|
erpnext.patches.v11_0.ewaybill_fields_gst_india #2018-11-13 #2019-01-09
|
||||||
erpnext.patches.v11_0.drop_column_max_days_allowed
|
erpnext.patches.v11_0.drop_column_max_days_allowed
|
||||||
erpnext.patches.v11_0.change_healthcare_desktop_icons
|
erpnext.patches.v11_0.change_healthcare_desktop_icons
|
||||||
erpnext.patches.v10_0.update_user_image_in_employee
|
erpnext.patches.v10_0.update_user_image_in_employee
|
||||||
@ -580,3 +580,5 @@ erpnext.patches.v11_0.update_delivery_trip_status
|
|||||||
erpnext.patches.v10_0.repost_gle_for_purchase_receipts_with_rejected_items
|
erpnext.patches.v10_0.repost_gle_for_purchase_receipts_with_rejected_items
|
||||||
erpnext.patches.v11_0.set_missing_gst_hsn_code
|
erpnext.patches.v11_0.set_missing_gst_hsn_code
|
||||||
erpnext.patches.v11_0.rename_bom_wo_fields
|
erpnext.patches.v11_0.rename_bom_wo_fields
|
||||||
|
erpnext.patches.v11_0.rename_additional_salary_component_additional_salary
|
||||||
|
erpnext.patches.v11_0.renamed_from_to_fields_in_project
|
@ -0,0 +1,10 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
# this patch should have been included with this PR https://github.com/frappe/erpnext/pull/14302
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
if frappe.db.table_exists("Additional Salary Component"):
|
||||||
|
if not frappe.db.table_exists("Additional Salary"):
|
||||||
|
frappe.rename_doc("DocType", "Additional Salary Component", "Additional Salary")
|
||||||
|
|
||||||
|
frappe.delete_doc('DocType', "Additional Salary Component")
|
@ -1,3 +1,4 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.model.rename_doc import rename_doc
|
from frappe.model.rename_doc import rename_doc
|
||||||
from frappe.model.utils.rename_field import rename_field
|
from frappe.model.utils.rename_field import rename_field
|
||||||
|
13
erpnext/patches/v11_0/renamed_from_to_fields_in_project.py
Normal file
13
erpnext/patches/v11_0/renamed_from_to_fields_in_project.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import frappe
|
||||||
|
from frappe.model.utils.rename_field import rename_field
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc('projects', 'doctype', 'project')
|
||||||
|
|
||||||
|
if frappe.db.has_column('Project', 'from'):
|
||||||
|
rename_field('Project', 'from', 'from_time')
|
||||||
|
rename_field('Project', 'to', 'to_time')
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"allow_copy": 0,
|
||||||
|
"allow_events_in_timeline": 0,
|
||||||
"allow_guest_to_view": 0,
|
"allow_guest_to_view": 0,
|
||||||
"allow_import": 1,
|
"allow_import": 1,
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
@ -1486,6 +1487,40 @@
|
|||||||
"translatable": 0,
|
"translatable": 0,
|
||||||
"unique": 0
|
"unique": 0
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 0,
|
||||||
|
"columns": 0,
|
||||||
|
"depends_on": "collect_progress",
|
||||||
|
"fieldname": "holiday_list",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"hidden": 0,
|
||||||
|
"ignore_user_permissions": 0,
|
||||||
|
"ignore_xss_filter": 0,
|
||||||
|
"in_filter": 0,
|
||||||
|
"in_global_search": 0,
|
||||||
|
"in_list_view": 0,
|
||||||
|
"in_standard_filter": 0,
|
||||||
|
"label": "Holiday List",
|
||||||
|
"length": 0,
|
||||||
|
"no_copy": 0,
|
||||||
|
"options": "Holiday List",
|
||||||
|
"permlevel": 0,
|
||||||
|
"precision": "",
|
||||||
|
"print_hide": 0,
|
||||||
|
"print_hide_if_no_value": 0,
|
||||||
|
"read_only": 0,
|
||||||
|
"remember_last_selected_value": 0,
|
||||||
|
"report_hide": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"search_index": 0,
|
||||||
|
"set_only_once": 0,
|
||||||
|
"translatable": 0,
|
||||||
|
"unique": 0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"allow_bulk_edit": 0,
|
||||||
"allow_in_quick_entry": 0,
|
"allow_in_quick_entry": 0,
|
||||||
@ -1527,8 +1562,9 @@
|
|||||||
"bold": 0,
|
"bold": 0,
|
||||||
"collapsible": 0,
|
"collapsible": 0,
|
||||||
"columns": 0,
|
"columns": 0,
|
||||||
"fieldname": "column_break_45",
|
"depends_on": "eval:(doc.frequency == \"Hourly\" && doc.collect_progress)",
|
||||||
"fieldtype": "Column Break",
|
"fieldname": "from_time",
|
||||||
|
"fieldtype": "Time",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
"ignore_user_permissions": 0,
|
"ignore_user_permissions": 0,
|
||||||
"ignore_xss_filter": 0,
|
"ignore_xss_filter": 0,
|
||||||
@ -1536,6 +1572,7 @@
|
|||||||
"in_global_search": 0,
|
"in_global_search": 0,
|
||||||
"in_list_view": 0,
|
"in_list_view": 0,
|
||||||
"in_standard_filter": 0,
|
"in_standard_filter": 0,
|
||||||
|
"label": "From Time",
|
||||||
"length": 0,
|
"length": 0,
|
||||||
"no_copy": 0,
|
"no_copy": 0,
|
||||||
"permlevel": 0,
|
"permlevel": 0,
|
||||||
@ -1558,8 +1595,8 @@
|
|||||||
"bold": 0,
|
"bold": 0,
|
||||||
"collapsible": 0,
|
"collapsible": 0,
|
||||||
"columns": 0,
|
"columns": 0,
|
||||||
"depends_on": "eval:(doc.frequency == \"Hourly\" && doc.collect_progress == true)",
|
"depends_on": "eval:(doc.frequency == \"Hourly\" && doc.collect_progress)",
|
||||||
"fieldname": "from",
|
"fieldname": "to_time",
|
||||||
"fieldtype": "Time",
|
"fieldtype": "Time",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
"ignore_user_permissions": 0,
|
"ignore_user_permissions": 0,
|
||||||
@ -1568,40 +1605,7 @@
|
|||||||
"in_global_search": 0,
|
"in_global_search": 0,
|
||||||
"in_list_view": 0,
|
"in_list_view": 0,
|
||||||
"in_standard_filter": 0,
|
"in_standard_filter": 0,
|
||||||
"label": "From",
|
"label": "To Time",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"depends_on": "eval:(doc.frequency == \"Hourly\" && doc.collect_progress == true)",
|
|
||||||
"fieldname": "to",
|
|
||||||
"fieldtype": "Time",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "To",
|
|
||||||
"length": 0,
|
"length": 0,
|
||||||
"no_copy": 0,
|
"no_copy": 0,
|
||||||
"permlevel": 0,
|
"permlevel": 0,
|
||||||
@ -1782,6 +1786,71 @@
|
|||||||
"set_only_once": 0,
|
"set_only_once": 0,
|
||||||
"translatable": 0,
|
"translatable": 0,
|
||||||
"unique": 0
|
"unique": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 0,
|
||||||
|
"columns": 0,
|
||||||
|
"fieldname": "column_break_45",
|
||||||
|
"fieldtype": "Column Break",
|
||||||
|
"hidden": 0,
|
||||||
|
"ignore_user_permissions": 0,
|
||||||
|
"ignore_xss_filter": 0,
|
||||||
|
"in_filter": 0,
|
||||||
|
"in_global_search": 0,
|
||||||
|
"in_list_view": 0,
|
||||||
|
"in_standard_filter": 0,
|
||||||
|
"length": 0,
|
||||||
|
"no_copy": 0,
|
||||||
|
"permlevel": 0,
|
||||||
|
"precision": "",
|
||||||
|
"print_hide": 0,
|
||||||
|
"print_hide_if_no_value": 0,
|
||||||
|
"read_only": 0,
|
||||||
|
"remember_last_selected_value": 0,
|
||||||
|
"report_hide": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"search_index": 0,
|
||||||
|
"set_only_once": 0,
|
||||||
|
"translatable": 0,
|
||||||
|
"unique": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 0,
|
||||||
|
"columns": 0,
|
||||||
|
"depends_on": "collect_progress",
|
||||||
|
"description": "Message will sent to users to get their status on the project",
|
||||||
|
"fieldname": "message",
|
||||||
|
"fieldtype": "Text",
|
||||||
|
"hidden": 0,
|
||||||
|
"ignore_user_permissions": 0,
|
||||||
|
"ignore_xss_filter": 0,
|
||||||
|
"in_filter": 0,
|
||||||
|
"in_global_search": 0,
|
||||||
|
"in_list_view": 0,
|
||||||
|
"in_standard_filter": 0,
|
||||||
|
"label": "Message",
|
||||||
|
"length": 0,
|
||||||
|
"no_copy": 0,
|
||||||
|
"permlevel": 0,
|
||||||
|
"precision": "",
|
||||||
|
"print_hide": 0,
|
||||||
|
"print_hide_if_no_value": 0,
|
||||||
|
"read_only": 0,
|
||||||
|
"remember_last_selected_value": 0,
|
||||||
|
"report_hide": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"search_index": 0,
|
||||||
|
"set_only_once": 0,
|
||||||
|
"translatable": 0,
|
||||||
|
"unique": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
"has_web_view": 0,
|
||||||
@ -1795,7 +1864,7 @@
|
|||||||
"issingle": 0,
|
"issingle": 0,
|
||||||
"istable": 0,
|
"istable": 0,
|
||||||
"max_attachments": 4,
|
"max_attachments": 4,
|
||||||
"modified": "2018-08-30 00:12:09.649654",
|
"modified": "2019-01-16 23:26:57.376682",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Projects",
|
"module": "Projects",
|
||||||
"name": "Project",
|
"name": "Project",
|
||||||
|
@ -3,16 +3,16 @@
|
|||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
from frappe.utils import flt, getdate, get_url, now
|
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from six import iteritems
|
||||||
from frappe.model.document import Document
|
from email_reply_parser import EmailReplyParser
|
||||||
|
from frappe.utils import (flt, getdate, get_url, now,
|
||||||
|
nowtime, get_time, today, get_datetime, add_days)
|
||||||
from erpnext.controllers.queries import get_filters_cond
|
from erpnext.controllers.queries import get_filters_cond
|
||||||
from frappe.desk.reportview import get_match_cond
|
from frappe.desk.reportview import get_match_cond
|
||||||
import datetime
|
from erpnext.hr.doctype.daily_work_summary.daily_work_summary import get_users_email
|
||||||
|
from erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group import is_holiday_today
|
||||||
from six import iteritems
|
from frappe.model.document import Document
|
||||||
|
|
||||||
class Project(Document):
|
class Project(Document):
|
||||||
def get_feed(self):
|
def get_feed(self):
|
||||||
@ -406,56 +406,137 @@ def get_users_for_project(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
def get_cost_center_name(project):
|
def get_cost_center_name(project):
|
||||||
return frappe.db.get_value("Project", project, "cost_center")
|
return frappe.db.get_value("Project", project, "cost_center")
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def hourly_reminder():
|
def hourly_reminder():
|
||||||
project = frappe.db.sql("""SELECT `tabProject`.name FROM `tabProject` WHERE `tabProject`.frequency = "Hourly" and (CURTIME() BETWEEN `tabProject`.from and `tabProject`.to) AND `tabProject`.collect_progress = 1 ORDER BY `tabProject`.name;""")
|
fields = ["from_time", "to_time"]
|
||||||
create_project_update(project)
|
projects = get_projects_for_collect_progress("Hourly", fields)
|
||||||
|
|
||||||
@frappe.whitelist()
|
for project in projects:
|
||||||
def twice_daily_reminder():
|
if (get_time(nowtime()) >= get_time(project.from_time) or
|
||||||
project = frappe.db.sql("""SELECT `tabProject User`.user FROM `tabProject User` INNER JOIN `tabProject` ON `tabProject`.project_name = `tabProject User`.parent WHERE (`tabProject`.frequency = "Twice Daily") AND ((`tabProject`.first_email BETWEEN DATE_ADD(curtime(), INTERVAL -15 MINUTE) AND DATE_ADD(curtime(), INTERVAL 15 MINUTE)) OR (`tabProject`.second_email BETWEEN DATE_ADD(curtime(), INTERVAL -15 MINUTE) AND DATE_ADD(curtime(), INTERVAL 15 MINUTE))) AND `tabProject`.collect_progress = 1;""")
|
get_time(nowtime()) <= get_time(project.to_time)):
|
||||||
create_project_update(project)
|
send_project_update_email_to_users(project.name)
|
||||||
|
|
||||||
|
def project_status_update_reminder():
|
||||||
|
daily_reminder()
|
||||||
|
twice_daily_reminder()
|
||||||
|
weekly_reminder()
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def daily_reminder():
|
def daily_reminder():
|
||||||
project = frappe.db.sql("""SELECT `tabProject User`.user FROM `tabProject User` INNER JOIN `tabProject` ON `tabProject`.project_name = `tabProject User`.parent WHERE (`tabProject`.frequency = "Daily") AND (`tabProject`.daily_time_to_send BETWEEN DATE_ADD(curtime(), INTERVAL -15 MINUTE) AND DATE_ADD(curtime(), INTERVAL 15 MINUTE)) AND `tabProject`.collect_progress = 1;""")
|
fields = ["daily_time_to_send"]
|
||||||
create_project_update(project)
|
projects = get_projects_for_collect_progress("Daily", fields)
|
||||||
|
|
||||||
@frappe.whitelist()
|
for project in projects:
|
||||||
def weekly():
|
if not check_project_update_exists(project.name, project.get("daily_time_to_send")):
|
||||||
today = datetime.datetime.now().strftime("%A")
|
send_project_update_email_to_users(project.name)
|
||||||
project = frappe.db.sql("""SELECT `tabProject User`.user FROM `tabProject User` INNER JOIN `tabProject` ON `tabProject`.project_name = `tabProject User`.parent WHERE (`tabProject`.frequency = "Weekly") AND (`tabProject`.day_to_send = %s) AND (`tabProject`.weekly_time_to_send BETWEEN DATE_ADD(curtime(), INTERVAL -15 MINUTE) AND DATE_ADD(curtime(), INTERVAL 15 MINUTE)) AND `tabProject`.collect_progress = 1""", today)
|
|
||||||
create_project_update(project)
|
|
||||||
|
|
||||||
#Call this function in order to generate the Project Update for a specific project
|
def twice_daily_reminder():
|
||||||
def create_project_update(project):
|
fields = ["first_email", "second_email"]
|
||||||
data = []
|
projects = get_projects_for_collect_progress("Twice Daily", fields)
|
||||||
date_today = datetime.date.today()
|
|
||||||
time_now = frappe.utils.now_datetime().strftime('%H:%M:%S')
|
for project in projects:
|
||||||
for projects in project:
|
for d in fields:
|
||||||
project_update_dict = {
|
if not check_project_update_exists(project.name, project.get(d)):
|
||||||
|
send_project_update_email_to_users(project.name)
|
||||||
|
|
||||||
|
def weekly_reminder():
|
||||||
|
fields = ["day_to_send", "weekly_time_to_send"]
|
||||||
|
projects = get_projects_for_collect_progress("Weekly", fields)
|
||||||
|
|
||||||
|
current_day = get_datetime().strftime("%A")
|
||||||
|
for project in projects:
|
||||||
|
if current_day != project.day_to_send:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not check_project_update_exists(project.name, project.get("weekly_time_to_send")):
|
||||||
|
send_project_update_email_to_users(project.name)
|
||||||
|
|
||||||
|
def check_project_update_exists(project, time):
|
||||||
|
data = frappe.db.sql(""" SELECT name from `tabProject Update`
|
||||||
|
WHERE project = %s and date = %s and time >= %s """, (project, today(), time))
|
||||||
|
|
||||||
|
return True if data and data[0][0] else False
|
||||||
|
|
||||||
|
def get_projects_for_collect_progress(frequency, fields):
|
||||||
|
fields.extend(["name"])
|
||||||
|
|
||||||
|
return frappe.get_all("Project", fields = fields,
|
||||||
|
filters = {'collect_progress': 1, 'frequency': frequency})
|
||||||
|
|
||||||
|
def send_project_update_email_to_users(project):
|
||||||
|
doc = frappe.get_doc('Project', project)
|
||||||
|
|
||||||
|
if is_holiday_today(doc.holiday_list) or not doc.users: return
|
||||||
|
|
||||||
|
project_update = frappe.get_doc({
|
||||||
"doctype" : "Project Update",
|
"doctype" : "Project Update",
|
||||||
"project" : projects[0],
|
"project" : project,
|
||||||
"date": date_today,
|
"sent": 0,
|
||||||
"time": time_now,
|
"date": today(),
|
||||||
"naming_series": "UPDATE-.project.-.YY.MM.DD.-"
|
"time": nowtime(),
|
||||||
}
|
"naming_series": "UPDATE-.project.-.YY.MM.DD.-",
|
||||||
project_update = frappe.get_doc(project_update_dict)
|
}).insert()
|
||||||
project_update.insert()
|
|
||||||
#you can edit your local_host
|
|
||||||
local_host = "http://localhost:8003"
|
|
||||||
project_update_url = "<a class = 'btn btn-primary' href=%s target='_blank'>" % (local_host +"/desk#Form/Project%20Update/" + (project_update.name)) + ("CREATE PROJECT UPDATE" + "</a>")
|
|
||||||
data.append(project_update_url)
|
|
||||||
|
|
||||||
email = frappe.db.sql("""SELECT user from `tabProject User` WHERE parent = %s;""", project[0])
|
subject = "For project %s, update your status" % (project)
|
||||||
for emails in email:
|
|
||||||
frappe.sendmail(
|
incoming_email_account = frappe.db.get_value('Email Account',
|
||||||
recipients=emails,
|
dict(enable_incoming=1, default_incoming=1), 'email_id')
|
||||||
subject=frappe._(projects[0]),
|
|
||||||
header=[frappe._("Please Update your Project Status"), 'blue'],
|
frappe.sendmail(recipients=get_users_email(doc),
|
||||||
message= project_update_url
|
message=doc.message,
|
||||||
|
subject=_(subject),
|
||||||
|
reference_doctype=project_update.doctype,
|
||||||
|
reference_name=project_update.name,
|
||||||
|
reply_to=incoming_email_account
|
||||||
)
|
)
|
||||||
return data
|
|
||||||
|
def collect_project_status():
|
||||||
|
for data in frappe.get_all("Project Update",
|
||||||
|
{'date': today(), 'sent': 0}):
|
||||||
|
replies = frappe.get_all('Communication',
|
||||||
|
fields=['content', 'text_content', 'sender'],
|
||||||
|
filters=dict(reference_doctype="Project Update",
|
||||||
|
reference_name=data.name,
|
||||||
|
communication_type='Communication',
|
||||||
|
sent_or_received='Received'),
|
||||||
|
order_by='creation asc')
|
||||||
|
|
||||||
|
for d in replies:
|
||||||
|
doc = frappe.get_doc("Project Update", data.name)
|
||||||
|
user_data = frappe.db.get_values("User", {"email": d.sender},
|
||||||
|
["full_name", "user_image", "name"], as_dict=True)[0]
|
||||||
|
|
||||||
|
doc.append("users", {
|
||||||
|
'user': user_data.name,
|
||||||
|
'full_name': user_data.full_name,
|
||||||
|
'image': user_data.user_image,
|
||||||
|
'project_status': frappe.utils.md_to_html(
|
||||||
|
EmailReplyParser.parse_reply(d.text_content) or d.content
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
doc.save(ignore_permissions=True)
|
||||||
|
|
||||||
|
def send_project_status_email_to_users():
|
||||||
|
yesterday = add_days(today(), -1)
|
||||||
|
|
||||||
|
for d in frappe.get_all("Project Update",
|
||||||
|
{'date': yesterday, 'sent': 0}):
|
||||||
|
doc = frappe.get_doc("Project Update", d.name)
|
||||||
|
|
||||||
|
project_doc = frappe.get_doc('Project', doc.project)
|
||||||
|
|
||||||
|
args = {
|
||||||
|
"users": doc.users,
|
||||||
|
"title": _("Project Summary for {0}").format(yesterday)
|
||||||
|
}
|
||||||
|
|
||||||
|
frappe.sendmail(recipients=get_users_email(project_doc),
|
||||||
|
template='daily_project_summary',
|
||||||
|
args=args,
|
||||||
|
subject=_("Daily Project Summary for {0}").format(d.name),
|
||||||
|
reference_doctype="Project Update",
|
||||||
|
reference_name=d.name)
|
||||||
|
|
||||||
|
doc.db_set('sent', 1)
|
||||||
|
|
||||||
def update_project_sales_billing():
|
def update_project_sales_billing():
|
||||||
sales_update_frequency = frappe.db.get_single_value("Selling Settings", "sales_update_frequency")
|
sales_update_frequency = frappe.db.get_single_value("Selling Settings", "sales_update_frequency")
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"allow_copy": 0,
|
||||||
|
"allow_events_in_timeline": 0,
|
||||||
"allow_guest_to_view": 0,
|
"allow_guest_to_view": 0,
|
||||||
"allow_import": 0,
|
"allow_import": 0,
|
||||||
"allow_rename": 0,
|
"allow_rename": 0,
|
||||||
@ -13,6 +14,40 @@
|
|||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"fields": [
|
"fields": [
|
||||||
|
{
|
||||||
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 0,
|
||||||
|
"columns": 0,
|
||||||
|
"default": "",
|
||||||
|
"fieldname": "naming_series",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 1,
|
||||||
|
"ignore_user_permissions": 0,
|
||||||
|
"ignore_xss_filter": 0,
|
||||||
|
"in_filter": 0,
|
||||||
|
"in_global_search": 0,
|
||||||
|
"in_list_view": 0,
|
||||||
|
"in_standard_filter": 0,
|
||||||
|
"label": "Series",
|
||||||
|
"length": 0,
|
||||||
|
"no_copy": 0,
|
||||||
|
"options": "PROJ-UPD-.YYYY.-",
|
||||||
|
"permlevel": 0,
|
||||||
|
"precision": "",
|
||||||
|
"print_hide": 0,
|
||||||
|
"print_hide_if_no_value": 0,
|
||||||
|
"read_only": 0,
|
||||||
|
"remember_last_selected_value": 0,
|
||||||
|
"report_hide": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"search_index": 0,
|
||||||
|
"set_only_once": 0,
|
||||||
|
"translatable": 0,
|
||||||
|
"unique": 0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"allow_bulk_edit": 0,
|
||||||
"allow_in_quick_entry": 0,
|
"allow_in_quick_entry": 0,
|
||||||
@ -46,6 +81,39 @@
|
|||||||
"translatable": 0,
|
"translatable": 0,
|
||||||
"unique": 0
|
"unique": 0
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 0,
|
||||||
|
"columns": 0,
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "sent",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"hidden": 0,
|
||||||
|
"ignore_user_permissions": 0,
|
||||||
|
"ignore_xss_filter": 0,
|
||||||
|
"in_filter": 0,
|
||||||
|
"in_global_search": 0,
|
||||||
|
"in_list_view": 0,
|
||||||
|
"in_standard_filter": 0,
|
||||||
|
"label": "Sent",
|
||||||
|
"length": 0,
|
||||||
|
"no_copy": 0,
|
||||||
|
"permlevel": 0,
|
||||||
|
"precision": "",
|
||||||
|
"print_hide": 0,
|
||||||
|
"print_hide_if_no_value": 0,
|
||||||
|
"read_only": 1,
|
||||||
|
"remember_last_selected_value": 0,
|
||||||
|
"report_hide": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"search_index": 0,
|
||||||
|
"set_only_once": 0,
|
||||||
|
"translatable": 0,
|
||||||
|
"unique": 0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"allow_bulk_edit": 0,
|
||||||
"allow_in_quick_entry": 0,
|
"allow_in_quick_entry": 0,
|
||||||
@ -172,39 +240,6 @@
|
|||||||
"translatable": 0,
|
"translatable": 0,
|
||||||
"unique": 0
|
"unique": 0
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "progress",
|
|
||||||
"fieldtype": "Select",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "How is the Project Progressing Right Now?",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Not Updated\nGreat/Quickly\nGood/Steady\nChallenging/Slow\nProblematic/Stuck",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"allow_bulk_edit": 0,
|
||||||
"allow_in_quick_entry": 0,
|
"allow_in_quick_entry": 0,
|
||||||
@ -238,38 +273,6 @@
|
|||||||
"translatable": 0,
|
"translatable": 0,
|
||||||
"unique": 0
|
"unique": 0
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "progress_details",
|
|
||||||
"fieldtype": "Text Editor",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Progress Details",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"allow_bulk_edit": 0,
|
||||||
"allow_in_quick_entry": 0,
|
"allow_in_quick_entry": 0,
|
||||||
@ -301,40 +304,6 @@
|
|||||||
"set_only_once": 0,
|
"set_only_once": 0,
|
||||||
"translatable": 0,
|
"translatable": 0,
|
||||||
"unique": 0
|
"unique": 0
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"default": "",
|
|
||||||
"fieldname": "naming_series",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 1,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Series",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "PROJ-UPD-.YYYY.-",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
"has_web_view": 0,
|
||||||
@ -347,7 +316,7 @@
|
|||||||
"issingle": 0,
|
"issingle": 0,
|
||||||
"istable": 0,
|
"istable": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"modified": "2018-08-21 14:44:21.287709",
|
"modified": "2019-01-16 19:31:05.210656",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Projects",
|
"module": "Projects",
|
||||||
"name": "Project Update",
|
"name": "Project Update",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"allow_copy": 0,
|
||||||
|
"allow_events_in_timeline": 0,
|
||||||
"allow_guest_to_view": 0,
|
"allow_guest_to_view": 0,
|
||||||
"allow_import": 0,
|
"allow_import": 0,
|
||||||
"allow_rename": 0,
|
"allow_rename": 0,
|
||||||
@ -44,6 +45,136 @@
|
|||||||
"translatable": 0,
|
"translatable": 0,
|
||||||
"unique": 0
|
"unique": 0
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 0,
|
||||||
|
"columns": 0,
|
||||||
|
"fetch_from": "user.email",
|
||||||
|
"fieldname": "email",
|
||||||
|
"fieldtype": "Read Only",
|
||||||
|
"hidden": 0,
|
||||||
|
"ignore_user_permissions": 0,
|
||||||
|
"ignore_xss_filter": 0,
|
||||||
|
"in_filter": 0,
|
||||||
|
"in_global_search": 0,
|
||||||
|
"in_list_view": 0,
|
||||||
|
"in_standard_filter": 0,
|
||||||
|
"label": "Email",
|
||||||
|
"length": 0,
|
||||||
|
"no_copy": 0,
|
||||||
|
"permlevel": 0,
|
||||||
|
"precision": "",
|
||||||
|
"print_hide": 0,
|
||||||
|
"print_hide_if_no_value": 0,
|
||||||
|
"read_only": 0,
|
||||||
|
"remember_last_selected_value": 0,
|
||||||
|
"report_hide": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"search_index": 0,
|
||||||
|
"set_only_once": 0,
|
||||||
|
"translatable": 0,
|
||||||
|
"unique": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 0,
|
||||||
|
"columns": 0,
|
||||||
|
"fetch_from": "user.user_image",
|
||||||
|
"fieldname": "image",
|
||||||
|
"fieldtype": "Read Only",
|
||||||
|
"hidden": 1,
|
||||||
|
"ignore_user_permissions": 0,
|
||||||
|
"ignore_xss_filter": 0,
|
||||||
|
"in_filter": 0,
|
||||||
|
"in_global_search": 1,
|
||||||
|
"in_list_view": 0,
|
||||||
|
"in_standard_filter": 0,
|
||||||
|
"label": "Image",
|
||||||
|
"length": 0,
|
||||||
|
"no_copy": 0,
|
||||||
|
"permlevel": 0,
|
||||||
|
"precision": "",
|
||||||
|
"print_hide": 0,
|
||||||
|
"print_hide_if_no_value": 0,
|
||||||
|
"read_only": 0,
|
||||||
|
"remember_last_selected_value": 0,
|
||||||
|
"report_hide": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"search_index": 0,
|
||||||
|
"set_only_once": 0,
|
||||||
|
"translatable": 0,
|
||||||
|
"unique": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 0,
|
||||||
|
"columns": 0,
|
||||||
|
"fieldname": "column_break_2",
|
||||||
|
"fieldtype": "Column Break",
|
||||||
|
"hidden": 0,
|
||||||
|
"ignore_user_permissions": 0,
|
||||||
|
"ignore_xss_filter": 0,
|
||||||
|
"in_filter": 0,
|
||||||
|
"in_global_search": 0,
|
||||||
|
"in_list_view": 0,
|
||||||
|
"in_standard_filter": 0,
|
||||||
|
"length": 0,
|
||||||
|
"no_copy": 0,
|
||||||
|
"permlevel": 0,
|
||||||
|
"precision": "",
|
||||||
|
"print_hide": 0,
|
||||||
|
"print_hide_if_no_value": 0,
|
||||||
|
"read_only": 0,
|
||||||
|
"remember_last_selected_value": 0,
|
||||||
|
"report_hide": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"search_index": 0,
|
||||||
|
"set_only_once": 0,
|
||||||
|
"translatable": 0,
|
||||||
|
"unique": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 0,
|
||||||
|
"columns": 0,
|
||||||
|
"default": "user.full_name",
|
||||||
|
"fieldname": "full_name",
|
||||||
|
"fieldtype": "Read Only",
|
||||||
|
"hidden": 0,
|
||||||
|
"ignore_user_permissions": 0,
|
||||||
|
"ignore_xss_filter": 0,
|
||||||
|
"in_filter": 0,
|
||||||
|
"in_global_search": 0,
|
||||||
|
"in_list_view": 0,
|
||||||
|
"in_standard_filter": 0,
|
||||||
|
"label": "Full Name",
|
||||||
|
"length": 0,
|
||||||
|
"no_copy": 0,
|
||||||
|
"permlevel": 0,
|
||||||
|
"precision": "",
|
||||||
|
"print_hide": 0,
|
||||||
|
"print_hide_if_no_value": 0,
|
||||||
|
"read_only": 0,
|
||||||
|
"remember_last_selected_value": 0,
|
||||||
|
"report_hide": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"search_index": 0,
|
||||||
|
"set_only_once": 0,
|
||||||
|
"translatable": 0,
|
||||||
|
"unique": 0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"allow_bulk_edit": 0,
|
||||||
"allow_in_quick_entry": 0,
|
"allow_in_quick_entry": 0,
|
||||||
@ -107,6 +238,70 @@
|
|||||||
"set_only_once": 0,
|
"set_only_once": 0,
|
||||||
"translatable": 0,
|
"translatable": 0,
|
||||||
"unique": 0
|
"unique": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 0,
|
||||||
|
"columns": 0,
|
||||||
|
"fieldname": "section_break_5",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"hidden": 0,
|
||||||
|
"ignore_user_permissions": 0,
|
||||||
|
"ignore_xss_filter": 0,
|
||||||
|
"in_filter": 0,
|
||||||
|
"in_global_search": 0,
|
||||||
|
"in_list_view": 0,
|
||||||
|
"in_standard_filter": 0,
|
||||||
|
"length": 0,
|
||||||
|
"no_copy": 0,
|
||||||
|
"permlevel": 0,
|
||||||
|
"precision": "",
|
||||||
|
"print_hide": 0,
|
||||||
|
"print_hide_if_no_value": 0,
|
||||||
|
"read_only": 0,
|
||||||
|
"remember_last_selected_value": 0,
|
||||||
|
"report_hide": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"search_index": 0,
|
||||||
|
"set_only_once": 0,
|
||||||
|
"translatable": 0,
|
||||||
|
"unique": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 0,
|
||||||
|
"columns": 0,
|
||||||
|
"depends_on": "eval:parent.doctype == 'Project Update'",
|
||||||
|
"fieldname": "project_status",
|
||||||
|
"fieldtype": "Text",
|
||||||
|
"hidden": 0,
|
||||||
|
"ignore_user_permissions": 0,
|
||||||
|
"ignore_xss_filter": 0,
|
||||||
|
"in_filter": 0,
|
||||||
|
"in_global_search": 0,
|
||||||
|
"in_list_view": 0,
|
||||||
|
"in_standard_filter": 0,
|
||||||
|
"label": "Project Status",
|
||||||
|
"length": 0,
|
||||||
|
"no_copy": 0,
|
||||||
|
"permlevel": 0,
|
||||||
|
"precision": "",
|
||||||
|
"print_hide": 0,
|
||||||
|
"print_hide_if_no_value": 0,
|
||||||
|
"read_only": 0,
|
||||||
|
"remember_last_selected_value": 0,
|
||||||
|
"report_hide": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"search_index": 0,
|
||||||
|
"set_only_once": 0,
|
||||||
|
"translatable": 0,
|
||||||
|
"unique": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
"has_web_view": 0,
|
||||||
@ -119,7 +314,7 @@
|
|||||||
"issingle": 0,
|
"issingle": 0,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"modified": "2018-09-09 12:39:38.376816",
|
"modified": "2019-01-17 17:10:05.339735",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Projects",
|
"module": "Projects",
|
||||||
"name": "Project User",
|
"name": "Project User",
|
||||||
|
@ -12,6 +12,7 @@ from frappe.utils.nestedset import NestedSet
|
|||||||
|
|
||||||
|
|
||||||
class CircularReferenceError(frappe.ValidationError): pass
|
class CircularReferenceError(frappe.ValidationError): pass
|
||||||
|
class EndDateCannotBeGreaterThanProjectEndDateError(frappe.ValidationError): pass
|
||||||
|
|
||||||
class Task(NestedSet):
|
class Task(NestedSet):
|
||||||
nsm_parent_field = 'parent_task'
|
nsm_parent_field = 'parent_task'
|
||||||
@ -43,6 +44,12 @@ class Task(NestedSet):
|
|||||||
if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date):
|
if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date):
|
||||||
frappe.throw(_("'Actual Start Date' can not be greater than 'Actual End Date'"))
|
frappe.throw(_("'Actual Start Date' can not be greater than 'Actual End Date'"))
|
||||||
|
|
||||||
|
if(self.project):
|
||||||
|
if frappe.db.exists("Project", self.project):
|
||||||
|
expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date")
|
||||||
|
if self.exp_end_date and expected_end_date and getdate(self.exp_end_date) > getdate(expected_end_date) :
|
||||||
|
frappe.throw(_("Expected end date cannot be after Project: <b>'{0}'</b> Expected end date").format(self.project), EndDateCannotBeGreaterThanProjectEndDateError)
|
||||||
|
|
||||||
def validate_status(self):
|
def validate_status(self):
|
||||||
if self.status!=self.get_db_value("status") and self.status == "Closed":
|
if self.status!=self.get_db_value("status") and self.status == "Closed":
|
||||||
for d in self.depends_on:
|
for d in self.depends_on:
|
||||||
|
@ -5,11 +5,11 @@ import frappe
|
|||||||
import unittest
|
import unittest
|
||||||
from frappe.utils import getdate, nowdate, add_days
|
from frappe.utils import getdate, nowdate, add_days
|
||||||
|
|
||||||
from erpnext.projects.doctype.task.task import CircularReferenceError
|
from erpnext.projects.doctype.task.task import CircularReferenceError, EndDateCannotBeGreaterThanProjectEndDateError
|
||||||
|
|
||||||
class TestTask(unittest.TestCase):
|
class TestTask(unittest.TestCase):
|
||||||
def test_circular_reference(self):
|
def test_circular_reference(self):
|
||||||
task1 = create_task("_Test Task 1", nowdate(), add_days(nowdate(), 10))
|
task1 = create_task("_Test Task 1", add_days(nowdate(), -15), add_days(nowdate(), -10))
|
||||||
task2 = create_task("_Test Task 2", add_days(nowdate(), 11), add_days(nowdate(), 15), task1.name)
|
task2 = create_task("_Test Task 2", add_days(nowdate(), 11), add_days(nowdate(), 15), task1.name)
|
||||||
task3 = create_task("_Test Task 3", add_days(nowdate(), 11), add_days(nowdate(), 15), task2.name)
|
task3 = create_task("_Test Task 3", add_days(nowdate(), 11), add_days(nowdate(), 15), task2.name)
|
||||||
|
|
||||||
@ -97,7 +97,16 @@ class TestTask(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(frappe.db.get_value("Task", task.name, "status"), "Overdue")
|
self.assertEqual(frappe.db.get_value("Task", task.name, "status"), "Overdue")
|
||||||
|
|
||||||
def create_task(subject, start=None, end=None, depends_on=None, project=None):
|
def test_end_date_validation(self):
|
||||||
|
task_end = create_task("Testing_Enddate_validation", add_days(nowdate(), 35), add_days(nowdate(), 45), save=False)
|
||||||
|
pro = frappe.get_doc("Project", task_end.project)
|
||||||
|
pro.expected_end_date = add_days(nowdate(), 40)
|
||||||
|
pro.save()
|
||||||
|
self.assertRaises(EndDateCannotBeGreaterThanProjectEndDateError, task_end.save)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def create_task(subject, start=None, end=None, depends_on=None, project=None, save=True):
|
||||||
if not frappe.db.exists("Task", subject):
|
if not frappe.db.exists("Task", subject):
|
||||||
task = frappe.new_doc('Task')
|
task = frappe.new_doc('Task')
|
||||||
task.status = "Open"
|
task.status = "Open"
|
||||||
@ -105,6 +114,7 @@ def create_task(subject, start=None, end=None, depends_on=None, project=None):
|
|||||||
task.exp_start_date = start or nowdate()
|
task.exp_start_date = start or nowdate()
|
||||||
task.exp_end_date = end or nowdate()
|
task.exp_end_date = end or nowdate()
|
||||||
task.project = project or "_Test Project"
|
task.project = project or "_Test Project"
|
||||||
|
if save:
|
||||||
task.save()
|
task.save()
|
||||||
else:
|
else:
|
||||||
task = frappe.get_doc("Task", subject)
|
task = frappe.get_doc("Task", subject)
|
||||||
@ -113,6 +123,7 @@ def create_task(subject, start=None, end=None, depends_on=None, project=None):
|
|||||||
task.append("depends_on", {
|
task.append("depends_on", {
|
||||||
"task": depends_on
|
"task": depends_on
|
||||||
})
|
})
|
||||||
|
if save:
|
||||||
task.save()
|
task.save()
|
||||||
|
|
||||||
return task
|
return task
|
@ -28,7 +28,10 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
|
|||||||
|
|
||||||
if (this.frm.doc.__islocal
|
if (this.frm.doc.__islocal
|
||||||
&& frappe.meta.has_field(this.frm.doc.doctype, "disable_rounded_total")) {
|
&& frappe.meta.has_field(this.frm.doc.doctype, "disable_rounded_total")) {
|
||||||
this.frm.set_value("disable_rounded_total", cint(frappe.sys_defaults.disable_rounded_total));
|
|
||||||
|
var df = frappe.meta.get_docfield(this.frm.doc.doctype, "disable_rounded_total");
|
||||||
|
var disable = df.default || cint(frappe.sys_defaults.disable_rounded_total);
|
||||||
|
this.frm.set_value("disable_rounded_total", disable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
@ -427,4 +430,3 @@ erpnext.buying.get_items_from_product_bundle = function(frm) {
|
|||||||
});
|
});
|
||||||
dialog.show();
|
dialog.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -416,6 +416,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
|||||||
item_code: item.item_code,
|
item_code: item.item_code,
|
||||||
barcode: item.barcode,
|
barcode: item.barcode,
|
||||||
serial_no: item.serial_no,
|
serial_no: item.serial_no,
|
||||||
|
set_warehouse: me.frm.doc.set_warehouse,
|
||||||
warehouse: item.warehouse,
|
warehouse: item.warehouse,
|
||||||
customer: me.frm.doc.customer,
|
customer: me.frm.doc.customer,
|
||||||
supplier: me.frm.doc.supplier,
|
supplier: me.frm.doc.supplier,
|
||||||
@ -440,6 +441,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
|||||||
weight_per_unit: item.weight_per_unit,
|
weight_per_unit: item.weight_per_unit,
|
||||||
weight_uom: item.weight_uom,
|
weight_uom: item.weight_uom,
|
||||||
uom : item.uom,
|
uom : item.uom,
|
||||||
|
stock_uom: item.stock_uom,
|
||||||
pos_profile: me.frm.doc.doctype == 'Sales Invoice' ? me.frm.doc.pos_profile : '',
|
pos_profile: me.frm.doc.doctype == 'Sales Invoice' ? me.frm.doc.pos_profile : '',
|
||||||
cost_center: item.cost_center
|
cost_center: item.cost_center
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
frappe.provide('erpnext.hub');
|
frappe.provide('erpnext.hub');
|
||||||
|
|
||||||
frappe.views.marketplaceFactory = class marketplaceFactory extends frappe.views.Factory {
|
frappe.views.MarketplaceFactory = class MarketplaceFactory extends frappe.views.Factory {
|
||||||
show() {
|
show() {
|
||||||
is_marketplace_disabled()
|
is_marketplace_disabled()
|
||||||
.then(disabled => {
|
.then(disabled => {
|
||||||
|
@ -7,5 +7,5 @@ from erpnext import get_region
|
|||||||
|
|
||||||
def check_deletion_permission(doc, method):
|
def check_deletion_permission(doc, method):
|
||||||
region = get_region()
|
region = get_region()
|
||||||
if region in ["Nepal", "France"]:
|
if region in ["Nepal", "France"] and doc.docstatus != 0:
|
||||||
frappe.throw(_("Deletion is not permitted for country {0}".format(region)))
|
frappe.throw(_("Deletion is not permitted for country {0}".format(region)))
|
@ -126,6 +126,9 @@ def make_custom_fields(update=True):
|
|||||||
dict(fieldname='place_of_supply', label='Place of Supply',
|
dict(fieldname='place_of_supply', label='Place of Supply',
|
||||||
fieldtype='Data', insert_after='shipping_address',
|
fieldtype='Data', insert_after='shipping_address',
|
||||||
print_hide=1, read_only=0),
|
print_hide=1, read_only=0),
|
||||||
|
]
|
||||||
|
|
||||||
|
purchase_invoice_itc_fields = [
|
||||||
dict(fieldname='eligibility_for_itc', label='Eligibility For ITC',
|
dict(fieldname='eligibility_for_itc', label='Eligibility For ITC',
|
||||||
fieldtype='Select', insert_after='reason_for_issuing_document', print_hide=1,
|
fieldtype='Select', insert_after='reason_for_issuing_document', print_hide=1,
|
||||||
options='input\ninput service\ncapital goods\nineligible', default="ineligible"),
|
options='input\ninput service\ncapital goods\nineligible', default="ineligible"),
|
||||||
@ -152,6 +155,9 @@ def make_custom_fields(update=True):
|
|||||||
dict(fieldname='company_gstin', label='Company GSTIN',
|
dict(fieldname='company_gstin', label='Company GSTIN',
|
||||||
fieldtype='Data', insert_after='company_address',
|
fieldtype='Data', insert_after='company_address',
|
||||||
fetch_from='company_address.gstin', print_hide=1),
|
fetch_from='company_address.gstin', print_hide=1),
|
||||||
|
]
|
||||||
|
|
||||||
|
sales_invoice_shipping_fields = [
|
||||||
dict(fieldname='port_code', label='Port Code',
|
dict(fieldname='port_code', label='Port Code',
|
||||||
fieldtype='Data', insert_after='reason_for_issuing_document', print_hide=1,
|
fieldtype='Data', insert_after='reason_for_issuing_document', print_hide=1,
|
||||||
depends_on="eval:doc.invoice_type=='Export' "),
|
depends_on="eval:doc.invoice_type=='Export' "),
|
||||||
@ -214,9 +220,12 @@ def make_custom_fields(update=True):
|
|||||||
dict(fieldname='gst_state_number', label='GST State Number',
|
dict(fieldname='gst_state_number', label='GST State Number',
|
||||||
fieldtype='Data', insert_after='gst_state', read_only=1),
|
fieldtype='Data', insert_after='gst_state', read_only=1),
|
||||||
],
|
],
|
||||||
'Purchase Invoice': invoice_gst_fields + purchase_invoice_gst_fields,
|
'Purchase Invoice': invoice_gst_fields + purchase_invoice_gst_fields + purchase_invoice_itc_fields,
|
||||||
'Sales Invoice': invoice_gst_fields + sales_invoice_gst_fields,
|
'Purchase Order': purchase_invoice_gst_fields,
|
||||||
'Delivery Note': sales_invoice_gst_fields + ewaybill_fields,
|
'Purchase Receipt': purchase_invoice_gst_fields,
|
||||||
|
'Sales Invoice': invoice_gst_fields + sales_invoice_gst_fields + sales_invoice_shipping_fields,
|
||||||
|
'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields,
|
||||||
|
'Sales Order': sales_invoice_gst_fields,
|
||||||
'Sales Taxes and Charges Template': inter_state_gst_field,
|
'Sales Taxes and Charges Template': inter_state_gst_field,
|
||||||
'Purchase Taxes and Charges Template': inter_state_gst_field,
|
'Purchase Taxes and Charges Template': inter_state_gst_field,
|
||||||
'Item': [
|
'Item': [
|
||||||
|
@ -11,23 +11,50 @@ def validate_gstin_for_india(doc, method):
|
|||||||
if not hasattr(doc, 'gstin'):
|
if not hasattr(doc, 'gstin'):
|
||||||
return
|
return
|
||||||
|
|
||||||
if doc.gstin:
|
doc.gstin = doc.gstin.upper().strip() if doc.gstin else ""
|
||||||
doc.gstin = doc.gstin.upper()
|
if not doc.gstin or doc.gstin == 'NA':
|
||||||
if doc.gstin not in ["NA", "na"]:
|
return
|
||||||
p = re.compile("[0-9]{2}[a-zA-Z]{5}[0-9]{4}[a-zA-Z]{1}[1-9A-Za-z]{1}[Z]{1}[0-9a-zA-Z]{1}")
|
|
||||||
|
if len(doc.gstin) != 15:
|
||||||
|
frappe.throw(_("Invalid GSTIN! A GSTIN must have 15 characters."))
|
||||||
|
|
||||||
|
p = re.compile("^[0-9]{2}[A-Z]{4}[0-9A-Z]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[1-9A-Z]{1}[0-9A-Z]{1}$")
|
||||||
if not p.match(doc.gstin):
|
if not p.match(doc.gstin):
|
||||||
frappe.throw(_("Invalid GSTIN or Enter NA for Unregistered"))
|
frappe.throw(_("Invalid GSTIN! The input you've entered doesn't match the format of GSTIN."))
|
||||||
|
|
||||||
|
validate_gstin_check_digit(doc.gstin)
|
||||||
|
|
||||||
if not doc.gst_state:
|
if not doc.gst_state:
|
||||||
if doc.state in states:
|
if not doc.state:
|
||||||
doc.gst_state = doc.state
|
return
|
||||||
|
state = doc.state.lower()
|
||||||
|
states_lowercase = {s.lower():s for s in states}
|
||||||
|
if state in states_lowercase:
|
||||||
|
doc.gst_state = states_lowercase[state]
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
if doc.gst_state:
|
|
||||||
doc.gst_state_number = state_numbers[doc.gst_state]
|
doc.gst_state_number = state_numbers[doc.gst_state]
|
||||||
if doc.gstin and doc.gstin != "NA" and doc.gst_state_number != doc.gstin[:2]:
|
if doc.gst_state_number != doc.gstin[:2]:
|
||||||
frappe.throw(_("First 2 digits of GSTIN should match with State number {0}")
|
frappe.throw(_("Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.")
|
||||||
.format(doc.gst_state_number))
|
.format(doc.gst_state_number))
|
||||||
|
|
||||||
|
def validate_gstin_check_digit(gstin):
|
||||||
|
''' Function to validate the check digit of the GSTIN.'''
|
||||||
|
factor = 1
|
||||||
|
total = 0
|
||||||
|
code_point_chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||||
|
mod = len(code_point_chars)
|
||||||
|
input_chars = gstin[:-1]
|
||||||
|
for char in input_chars:
|
||||||
|
digit = factor * code_point_chars.find(char)
|
||||||
|
digit = (digit // mod) + (digit % mod)
|
||||||
|
total += digit
|
||||||
|
factor = 2 if factor == 1 else 1
|
||||||
|
if gstin[-1] != code_point_chars[((mod - (total % mod)) % mod)]:
|
||||||
|
frappe.throw(_("Invalid GSTIN! The check digit validation has failed. " +
|
||||||
|
"Please ensure you've typed the GSTIN correctly."))
|
||||||
|
|
||||||
def get_itemised_tax_breakup_header(item_doctype, tax_accounts):
|
def get_itemised_tax_breakup_header(item_doctype, tax_accounts):
|
||||||
if frappe.get_meta(item_doctype).has_field('gst_hsn_code'):
|
if frappe.get_meta(item_doctype).has_field('gst_hsn_code'):
|
||||||
return [_("HSN/SAC"), _("Taxable Amount")] + tax_accounts
|
return [_("HSN/SAC"), _("Taxable Amount")] + tax_accounts
|
||||||
|
@ -120,7 +120,7 @@ class Gstr1Report(object):
|
|||||||
and customer in ('{0}')""".format("', '".join([frappe.db.escape(c.name) for c in customers]))
|
and customer in ('{0}')""".format("', '".join([frappe.db.escape(c.name) for c in customers]))
|
||||||
|
|
||||||
if self.filters.get("type_of_business") in ("B2C Large", "B2C Small"):
|
if self.filters.get("type_of_business") in ("B2C Large", "B2C Small"):
|
||||||
b2c_limit = frappe.db.get_single_value('GSt Settings', 'b2c_limit')
|
b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit')
|
||||||
if not b2c_limit:
|
if not b2c_limit:
|
||||||
frappe.throw(_("Please set B2C Limit in GST Settings."))
|
frappe.throw(_("Please set B2C Limit in GST Settings."))
|
||||||
|
|
||||||
|
@ -177,6 +177,11 @@ class Customer(TransactionBase):
|
|||||||
frappe.throw(_("""New credit limit is less than current outstanding amount for the customer. Credit limit has to be atleast {0}""").format(outstanding_amt))
|
frappe.throw(_("""New credit limit is less than current outstanding amount for the customer. Credit limit has to be atleast {0}""").format(outstanding_amt))
|
||||||
|
|
||||||
def on_trash(self):
|
def on_trash(self):
|
||||||
|
if self.customer_primary_contact:
|
||||||
|
frappe.db.sql("""update `tabCustomer`
|
||||||
|
set customer_primary_contact=null, mobile_no=null, email_id=null
|
||||||
|
where name=%s""", self.name)
|
||||||
|
|
||||||
delete_contact_and_address('Customer', self.name)
|
delete_contact_and_address('Customer', self.name)
|
||||||
if self.lead_name:
|
if self.lead_name:
|
||||||
frappe.db.sql("update `tabLead` set status='Interested' where name=%s", self.lead_name)
|
frappe.db.sql("update `tabLead` set status='Interested' where name=%s", self.lead_name)
|
||||||
|
@ -98,6 +98,15 @@ class TestCustomer(unittest.TestCase):
|
|||||||
|
|
||||||
so.save()
|
so.save()
|
||||||
|
|
||||||
|
def test_delete_customer_contact(self):
|
||||||
|
customer = frappe.get_doc(
|
||||||
|
get_customer_dict('_Test Customer for delete')).insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
customer.mobile_no = "8989889890"
|
||||||
|
customer.save()
|
||||||
|
self.assertTrue(customer.customer_primary_contact)
|
||||||
|
frappe.delete_doc('Customer', customer.name)
|
||||||
|
|
||||||
def test_disabled_customer(self):
|
def test_disabled_customer(self):
|
||||||
make_test_records("Item")
|
make_test_records("Item")
|
||||||
|
|
||||||
|
@ -31,6 +31,26 @@ class TestQuotation(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertFalse(sales_order.get('payment_schedule'))
|
self.assertFalse(sales_order.get('payment_schedule'))
|
||||||
|
|
||||||
|
def test_make_sales_order_with_different_currency(self):
|
||||||
|
from erpnext.selling.doctype.quotation.quotation import make_sales_order
|
||||||
|
|
||||||
|
quotation = frappe.copy_doc(test_records[0])
|
||||||
|
quotation.transaction_date = nowdate()
|
||||||
|
quotation.valid_till = add_months(quotation.transaction_date, 1)
|
||||||
|
quotation.insert()
|
||||||
|
quotation.submit()
|
||||||
|
|
||||||
|
sales_order = make_sales_order(quotation.name)
|
||||||
|
sales_order.currency = "USD"
|
||||||
|
sales_order.conversion_rate = 20.0
|
||||||
|
sales_order.delivery_date = "2019-01-01"
|
||||||
|
sales_order.naming_series = "_T-Quotation-"
|
||||||
|
sales_order.transaction_date = nowdate()
|
||||||
|
sales_order.insert()
|
||||||
|
|
||||||
|
self.assertEquals(sales_order.currency, "USD")
|
||||||
|
self.assertNotEqual(sales_order.currency, quotation.currency)
|
||||||
|
|
||||||
def test_make_sales_order(self):
|
def test_make_sales_order(self):
|
||||||
from erpnext.selling.doctype.quotation.quotation import make_sales_order
|
from erpnext.selling.doctype.quotation.quotation import make_sales_order
|
||||||
|
|
||||||
|
@ -17,6 +17,20 @@ frappe.ui.form.on("Sales Order", {
|
|||||||
// formatter for material request item
|
// formatter for material request item
|
||||||
frm.set_indicator_formatter('item_code',
|
frm.set_indicator_formatter('item_code',
|
||||||
function(doc) { return (doc.stock_qty<=doc.delivered_qty) ? "green" : "orange" })
|
function(doc) { return (doc.stock_qty<=doc.delivered_qty) ? "green" : "orange" })
|
||||||
|
|
||||||
|
frm.set_query('company_address', function(doc) {
|
||||||
|
if(!doc.company) {
|
||||||
|
frappe.throw(__('Please set Company'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
query: 'frappe.contacts.doctype.address.address.address_query',
|
||||||
|
filters: {
|
||||||
|
link_doctype: 'Company',
|
||||||
|
link_name: doc.company
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
},
|
},
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
if(frm.doc.docstatus == 1 && frm.doc.status == 'To Deliver and Bill') {
|
if(frm.doc.docstatus == 1 && frm.doc.status == 'To Deliver and Bill') {
|
||||||
@ -367,7 +381,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
var d = new frappe.ui.Dialog({
|
var d = new frappe.ui.Dialog({
|
||||||
title: __("Select from Items having BOM"),
|
title: __("Items for Raw Material Request"),
|
||||||
fields: fields,
|
fields: fields,
|
||||||
primary_action: function() {
|
primary_action: function() {
|
||||||
var data = d.get_values();
|
var data = d.get_values();
|
||||||
|
@ -747,6 +747,71 @@
|
|||||||
"translatable": 0,
|
"translatable": 0,
|
||||||
"unique": 0
|
"unique": 0
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 0,
|
||||||
|
"columns": 0,
|
||||||
|
"fieldname": "company_address_display",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"hidden": 0,
|
||||||
|
"ignore_user_permissions": 0,
|
||||||
|
"ignore_xss_filter": 0,
|
||||||
|
"in_filter": 0,
|
||||||
|
"in_global_search": 0,
|
||||||
|
"in_list_view": 0,
|
||||||
|
"in_standard_filter": 0,
|
||||||
|
"label": "",
|
||||||
|
"length": 0,
|
||||||
|
"no_copy": 0,
|
||||||
|
"permlevel": 0,
|
||||||
|
"precision": "",
|
||||||
|
"print_hide": 0,
|
||||||
|
"print_hide_if_no_value": 0,
|
||||||
|
"read_only": 1,
|
||||||
|
"remember_last_selected_value": 0,
|
||||||
|
"report_hide": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"search_index": 0,
|
||||||
|
"set_only_once": 0,
|
||||||
|
"translatable": 0,
|
||||||
|
"unique": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 0,
|
||||||
|
"columns": 0,
|
||||||
|
"fieldname": "company_address",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"hidden": 0,
|
||||||
|
"ignore_user_permissions": 0,
|
||||||
|
"ignore_xss_filter": 0,
|
||||||
|
"in_filter": 0,
|
||||||
|
"in_global_search": 0,
|
||||||
|
"in_list_view": 0,
|
||||||
|
"in_standard_filter": 0,
|
||||||
|
"label": "Company Address",
|
||||||
|
"length": 0,
|
||||||
|
"no_copy": 0,
|
||||||
|
"options": "Address",
|
||||||
|
"permlevel": 0,
|
||||||
|
"precision": "",
|
||||||
|
"print_hide": 0,
|
||||||
|
"print_hide_if_no_value": 0,
|
||||||
|
"read_only": 0,
|
||||||
|
"remember_last_selected_value": 0,
|
||||||
|
"report_hide": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"search_index": 0,
|
||||||
|
"set_only_once": 0,
|
||||||
|
"translatable": 0,
|
||||||
|
"unique": 0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"allow_bulk_edit": 0,
|
||||||
"allow_in_quick_entry": 0,
|
"allow_in_quick_entry": 0,
|
||||||
@ -1128,6 +1193,7 @@
|
|||||||
"label": "Price List Exchange Rate",
|
"label": "Price List Exchange Rate",
|
||||||
"length": 0,
|
"length": 0,
|
||||||
"no_copy": 0,
|
"no_copy": 0,
|
||||||
|
"options": "",
|
||||||
"permlevel": 0,
|
"permlevel": 0,
|
||||||
"precision": "9",
|
"precision": "9",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
@ -4015,7 +4081,7 @@
|
|||||||
"issingle": 0,
|
"issingle": 0,
|
||||||
"istable": 0,
|
"istable": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"modified": "2019-01-07 16:51:50.857438",
|
"modified": "2019-01-09 16:51:47.917329",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Sales Order",
|
"name": "Sales Order",
|
||||||
|
@ -147,7 +147,7 @@ class SalesOrder(SellingController):
|
|||||||
super(SalesOrder, self).validate_with_previous_doc({
|
super(SalesOrder, self).validate_with_previous_doc({
|
||||||
"Quotation": {
|
"Quotation": {
|
||||||
"ref_dn_field": "prevdoc_docname",
|
"ref_dn_field": "prevdoc_docname",
|
||||||
"compare_fields": [["company", "="], ["currency", "="]]
|
"compare_fields": [["company", "="]]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -372,11 +372,9 @@ class SalesOrder(SellingController):
|
|||||||
def get_work_order_items(self, for_raw_material_request=0):
|
def get_work_order_items(self, for_raw_material_request=0):
|
||||||
'''Returns items with BOM that already do not have a linked work order'''
|
'''Returns items with BOM that already do not have a linked work order'''
|
||||||
items = []
|
items = []
|
||||||
|
|
||||||
for table in [self.items, self.packed_items]:
|
for table in [self.items, self.packed_items]:
|
||||||
for i in table:
|
for i in table:
|
||||||
bom = get_default_bom_item(i.item_code)
|
bom = get_default_bom_item(i.item_code)
|
||||||
if bom:
|
|
||||||
stock_qty = i.qty if i.doctype == 'Packed Item' else i.stock_qty
|
stock_qty = i.qty if i.doctype == 'Packed Item' else i.stock_qty
|
||||||
if not for_raw_material_request:
|
if not for_raw_material_request:
|
||||||
total_work_order_qty = flt(frappe.db.sql('''select sum(qty) from `tabWork Order`
|
total_work_order_qty = flt(frappe.db.sql('''select sum(qty) from `tabWork Order`
|
||||||
@ -386,6 +384,7 @@ class SalesOrder(SellingController):
|
|||||||
pending_qty = stock_qty
|
pending_qty = stock_qty
|
||||||
|
|
||||||
if pending_qty:
|
if pending_qty:
|
||||||
|
if bom:
|
||||||
items.append(dict(
|
items.append(dict(
|
||||||
name= i.name,
|
name= i.name,
|
||||||
item_code= i.item_code,
|
item_code= i.item_code,
|
||||||
@ -395,6 +394,16 @@ class SalesOrder(SellingController):
|
|||||||
required_qty = pending_qty if for_raw_material_request else 0,
|
required_qty = pending_qty if for_raw_material_request else 0,
|
||||||
sales_order_item = i.name
|
sales_order_item = i.name
|
||||||
))
|
))
|
||||||
|
else:
|
||||||
|
items.append(dict(
|
||||||
|
name= i.name,
|
||||||
|
item_code= i.item_code,
|
||||||
|
bom = '',
|
||||||
|
warehouse = i.warehouse,
|
||||||
|
pending_qty = pending_qty,
|
||||||
|
required_qty = pending_qty if for_raw_material_request else 0,
|
||||||
|
sales_order_item = i.name
|
||||||
|
))
|
||||||
return items
|
return items
|
||||||
|
|
||||||
def on_recurring(self, reference_doc, auto_repeat_doc):
|
def on_recurring(self, reference_doc, auto_repeat_doc):
|
||||||
@ -923,10 +932,12 @@ def make_raw_material_request(items, company, sales_order, project=None):
|
|||||||
for item in items.get('items'):
|
for item in items.get('items'):
|
||||||
item["include_exploded_items"] = items.get('include_exploded_items')
|
item["include_exploded_items"] = items.get('include_exploded_items')
|
||||||
item["ignore_existing_ordered_qty"] = items.get('ignore_existing_ordered_qty')
|
item["ignore_existing_ordered_qty"] = items.get('ignore_existing_ordered_qty')
|
||||||
|
item["include_raw_materials_from_sales_order"] = items.get('include_raw_materials_from_sales_order')
|
||||||
|
|
||||||
raw_materials = get_items_for_material_requests(items, company)
|
raw_materials = get_items_for_material_requests(items, sales_order, company)
|
||||||
if not raw_materials:
|
if not raw_materials:
|
||||||
frappe.msgprint(_("Material Request not created, as quantity for Raw Materials already available."))
|
frappe.msgprint(_("Material Request not created, as quantity for Raw Materials already available."))
|
||||||
|
return
|
||||||
|
|
||||||
material_request = frappe.new_doc('Material Request')
|
material_request = frappe.new_doc('Material Request')
|
||||||
material_request.update(dict(
|
material_request.update(dict(
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, scrub
|
from frappe import _, scrub
|
||||||
from frappe.utils import getdate, flt
|
from frappe.utils import getdate, flt, add_to_date, add_days
|
||||||
from six import iteritems
|
from six import iteritems
|
||||||
from erpnext.accounts.utils import get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ class Analytics(object):
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"width": 140
|
"width": 140
|
||||||
})
|
})
|
||||||
for dummy, end_date in self.periodic_daterange:
|
for end_date in self.periodic_daterange:
|
||||||
period = self.get_period(end_date)
|
period = self.get_period(end_date)
|
||||||
self.columns.append({
|
self.columns.append({
|
||||||
"label": _(period),
|
"label": _(period),
|
||||||
@ -169,7 +169,7 @@ class Analytics(object):
|
|||||||
"entity_name": self.entity_names.get(entity)
|
"entity_name": self.entity_names.get(entity)
|
||||||
}
|
}
|
||||||
total = 0
|
total = 0
|
||||||
for dummy, end_date in self.periodic_daterange:
|
for end_date in self.periodic_daterange:
|
||||||
period = self.get_period(end_date)
|
period = self.get_period(end_date)
|
||||||
amount = flt(period_data.get(period, 0.0))
|
amount = flt(period_data.get(period, 0.0))
|
||||||
row[scrub(period)] = amount
|
row[scrub(period)] = amount
|
||||||
@ -188,7 +188,7 @@ class Analytics(object):
|
|||||||
"indent": self.depth_map.get(d.name)
|
"indent": self.depth_map.get(d.name)
|
||||||
}
|
}
|
||||||
total = 0
|
total = 0
|
||||||
for dummy, end_date in self.periodic_daterange:
|
for end_date in self.periodic_daterange:
|
||||||
period = self.get_period(end_date)
|
period = self.get_period(end_date)
|
||||||
amount = flt(self.entity_periodic_data.get(d.name, {}).get(period, 0.0))
|
amount = flt(self.entity_periodic_data.get(d.name, {}).get(period, 0.0))
|
||||||
row[scrub(period)] = amount
|
row[scrub(period)] = amount
|
||||||
@ -219,12 +219,11 @@ class Analytics(object):
|
|||||||
period = "Quarter " + str(((posting_date.month-1)//3)+1) +" " + str(posting_date.year)
|
period = "Quarter " + str(((posting_date.month-1)//3)+1) +" " + str(posting_date.year)
|
||||||
else:
|
else:
|
||||||
year = get_fiscal_year(posting_date, company=self.filters.company)
|
year = get_fiscal_year(posting_date, company=self.filters.company)
|
||||||
period = str(year[2])
|
period = str(year[0])
|
||||||
|
|
||||||
return period
|
return period
|
||||||
|
|
||||||
def get_period_date_ranges(self):
|
def get_period_date_ranges(self):
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta, MO
|
||||||
from_date, to_date = getdate(self.filters.from_date), getdate(self.filters.to_date)
|
from_date, to_date = getdate(self.filters.from_date), getdate(self.filters.to_date)
|
||||||
|
|
||||||
increment = {
|
increment = {
|
||||||
@ -234,18 +233,26 @@ class Analytics(object):
|
|||||||
"Yearly": 12
|
"Yearly": 12
|
||||||
}.get(self.filters.range, 1)
|
}.get(self.filters.range, 1)
|
||||||
|
|
||||||
self.periodic_daterange = []
|
if self.filters.range in ['Monthly', 'Quarterly']:
|
||||||
for dummy in range(1, 53, increment):
|
from_date = from_date.replace(day = 1)
|
||||||
if self.filters.range == "Weekly":
|
elif self.filters.range == "Yearly":
|
||||||
period_end_date = from_date + relativedelta(days=6)
|
from_date = get_fiscal_year(from_date)[1]
|
||||||
else:
|
else:
|
||||||
period_end_date = from_date + relativedelta(months=increment, days=-1)
|
from_date = from_date + relativedelta(from_date, weekday=MO(-1))
|
||||||
|
|
||||||
|
self.periodic_daterange = []
|
||||||
|
for dummy in range(1, 53):
|
||||||
|
if self.filters.range == "Weekly":
|
||||||
|
period_end_date = add_days(from_date, 6)
|
||||||
|
else:
|
||||||
|
period_end_date = add_to_date(from_date, months=increment, days=-1)
|
||||||
|
|
||||||
if period_end_date > to_date:
|
if period_end_date > to_date:
|
||||||
period_end_date = to_date
|
period_end_date = to_date
|
||||||
self.periodic_daterange.append([from_date, period_end_date])
|
|
||||||
|
|
||||||
from_date = period_end_date + relativedelta(days=1)
|
self.periodic_daterange.append(period_end_date)
|
||||||
|
|
||||||
|
from_date = add_days(period_end_date, 1)
|
||||||
if period_end_date == to_date:
|
if period_end_date == to_date:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
|
||||||
doctype_list = [
|
doctype_list = [
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"allow_copy": 0,
|
||||||
|
"allow_events_in_timeline": 0,
|
||||||
"allow_guest_to_view": 0,
|
"allow_guest_to_view": 0,
|
||||||
"allow_import": 1,
|
"allow_import": 1,
|
||||||
"allow_rename": 0,
|
"allow_rename": 0,
|
||||||
@ -352,7 +353,7 @@
|
|||||||
"fieldname": "to_emp",
|
"fieldname": "to_emp",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
"ignore_user_permissions": 0,
|
"ignore_user_permissions": 1,
|
||||||
"ignore_xss_filter": 0,
|
"ignore_xss_filter": 0,
|
||||||
"in_filter": 0,
|
"in_filter": 0,
|
||||||
"in_global_search": 0,
|
"in_global_search": 0,
|
||||||
@ -618,7 +619,7 @@
|
|||||||
"issingle": 0,
|
"issingle": 0,
|
||||||
"istable": 0,
|
"istable": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"modified": "2018-08-21 16:15:42.627233",
|
"modified": "2019-01-21 17:10:39.822125",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Setup",
|
"module": "Setup",
|
||||||
"name": "Authorization Rule",
|
"name": "Authorization Rule",
|
||||||
|
@ -13,7 +13,7 @@ def get_data():
|
|||||||
'goal_doctype_link': 'company',
|
'goal_doctype_link': 'company',
|
||||||
'goal_field': 'base_grand_total',
|
'goal_field': 'base_grand_total',
|
||||||
'date_field': 'posting_date',
|
'date_field': 'posting_date',
|
||||||
'filter_str': 'docstatus = 1',
|
'filter_str': "docstatus = 1 and is_opening != 'Yes'",
|
||||||
'aggregation': 'sum'
|
'aggregation': 'sum'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -136,7 +136,10 @@ def get_child_groups_for_list_in_html(item_group, start, limit, search):
|
|||||||
rgt = ('<', item_group.rgt),
|
rgt = ('<', item_group.rgt),
|
||||||
),
|
),
|
||||||
or_filters = search_filters,
|
or_filters = search_filters,
|
||||||
order_by = 'weightage desc, name asc')
|
order_by = 'weightage desc, name asc',
|
||||||
|
start = start,
|
||||||
|
limit = limit
|
||||||
|
)
|
||||||
|
|
||||||
return [get_item_for_list_in_html(r) for r in data]
|
return [get_item_for_list_in_html(r) for r in data]
|
||||||
|
|
||||||
@ -147,7 +150,7 @@ def adjust_qty_for_expired_items(data):
|
|||||||
if item.get('has_batch_no') and item.get('website_warehouse'):
|
if item.get('has_batch_no') and item.get('website_warehouse'):
|
||||||
stock_qty_dict = get_qty_in_stock(
|
stock_qty_dict = get_qty_in_stock(
|
||||||
item.get('name'), 'website_warehouse', item.get('website_warehouse'))
|
item.get('name'), 'website_warehouse', item.get('website_warehouse'))
|
||||||
qty = stock_qty_dict.stock_qty[0][0]
|
qty = stock_qty_dict.stock_qty[0][0] if stock_qty_dict.stock_qty else 0
|
||||||
item['in_stock'] = 1 if qty else 0
|
item['in_stock'] = 1 if qty else 0
|
||||||
adjusted_data.append(item)
|
adjusted_data.append(item)
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ def place_order():
|
|||||||
sales_order = frappe.get_doc(_make_sales_order(quotation.name, ignore_permissions=True))
|
sales_order = frappe.get_doc(_make_sales_order(quotation.name, ignore_permissions=True))
|
||||||
for item in sales_order.get("items"):
|
for item in sales_order.get("items"):
|
||||||
item.reserved_warehouse, is_stock_item = frappe.db.get_value("Item",
|
item.reserved_warehouse, is_stock_item = frappe.db.get_value("Item",
|
||||||
item.item_code, ["website_warehouse", "is_stock_item"]) or None, None
|
item.item_code, ["website_warehouse", "is_stock_item"])
|
||||||
|
|
||||||
if is_stock_item:
|
if is_stock_item:
|
||||||
item_stock = get_qty_in_stock(item.item_code, "website_warehouse")
|
item_stock = get_qty_in_stock(item.item_code, "website_warehouse")
|
||||||
|
@ -5,6 +5,7 @@ $.extend(cur_frm.cscript, {
|
|||||||
onload: function() {
|
onload: function() {
|
||||||
if(cur_frm.doc.__onload && cur_frm.doc.__onload.quotation_series) {
|
if(cur_frm.doc.__onload && cur_frm.doc.__onload.quotation_series) {
|
||||||
cur_frm.fields_dict.quotation_series.df.options = cur_frm.doc.__onload.quotation_series;
|
cur_frm.fields_dict.quotation_series.df.options = cur_frm.doc.__onload.quotation_series;
|
||||||
|
cur_frm.refresh_field("quotation_series");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
refresh: function(){
|
refresh: function(){
|
||||||
|
@ -378,7 +378,7 @@ def make_item_variant():
|
|||||||
|
|
||||||
test_records = frappe.get_test_records('Item')
|
test_records = frappe.get_test_records('Item')
|
||||||
|
|
||||||
def create_item(item_code, is_stock_item=None, valuation_rate=0, warehouse=None):
|
def create_item(item_code, is_stock_item=None, valuation_rate=0, warehouse=None, opening_stock=None):
|
||||||
if not frappe.db.exists("Item", item_code):
|
if not frappe.db.exists("Item", item_code):
|
||||||
item = frappe.new_doc("Item")
|
item = frappe.new_doc("Item")
|
||||||
item.item_code = item_code
|
item.item_code = item_code
|
||||||
@ -386,6 +386,7 @@ def create_item(item_code, is_stock_item=None, valuation_rate=0, warehouse=None)
|
|||||||
item.description = item_code
|
item.description = item_code
|
||||||
item.item_group = "All Item Groups"
|
item.item_group = "All Item Groups"
|
||||||
item.is_stock_item = is_stock_item or 1
|
item.is_stock_item = is_stock_item or 1
|
||||||
|
item.opening_stock = opening_stock or 0
|
||||||
item.valuation_rate = valuation_rate or 0.0
|
item.valuation_rate = valuation_rate or 0.0
|
||||||
item.append("item_defaults", {
|
item.append("item_defaults", {
|
||||||
"default_warehouse": warehouse or '_Test Warehouse - _TC',
|
"default_warehouse": warehouse or '_Test Warehouse - _TC',
|
||||||
|
@ -6,7 +6,7 @@ import unittest
|
|||||||
from frappe.utils import nowdate
|
from frappe.utils import nowdate
|
||||||
from erpnext.stock.doctype.item.test_item import create_item
|
from erpnext.stock.doctype.item.test_item import create_item
|
||||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||||
from erpnext.controllers.stock_controller import QualityInspectionRejectedError, QualityInspectionRequiredError
|
from erpnext.controllers.stock_controller import QualityInspectionRejectedError, QualityInspectionRequiredError, QualityInspectionNotSubmittedError
|
||||||
|
|
||||||
# test_records = frappe.get_test_records('Quality Inspection')
|
# test_records = frappe.get_test_records('Quality Inspection')
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ class TestQualityInspection(unittest.TestCase):
|
|||||||
dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
|
dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
|
||||||
self.assertRaises(QualityInspectionRequiredError, dn.submit)
|
self.assertRaises(QualityInspectionRequiredError, dn.submit)
|
||||||
|
|
||||||
qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, status="Rejected")
|
qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, status="Rejected", submit=True)
|
||||||
dn.reload()
|
dn.reload()
|
||||||
self.assertRaises(QualityInspectionRejectedError, dn.submit)
|
self.assertRaises(QualityInspectionRejectedError, dn.submit)
|
||||||
|
|
||||||
@ -27,6 +27,12 @@ class TestQualityInspection(unittest.TestCase):
|
|||||||
dn.reload()
|
dn.reload()
|
||||||
dn.submit()
|
dn.submit()
|
||||||
|
|
||||||
|
def test_qa_not_submit(self):
|
||||||
|
dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
|
||||||
|
qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, submit = False)
|
||||||
|
dn.items[0].quality_inspection = qa.name
|
||||||
|
self.assertRaises(QualityInspectionNotSubmittedError, dn.submit)
|
||||||
|
|
||||||
def create_quality_inspection(**args):
|
def create_quality_inspection(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
qa = frappe.new_doc("Quality Inspection")
|
qa = frappe.new_doc("Quality Inspection")
|
||||||
@ -42,6 +48,7 @@ def create_quality_inspection(**args):
|
|||||||
"status": args.status
|
"status": args.status
|
||||||
})
|
})
|
||||||
qa.save()
|
qa.save()
|
||||||
|
if args.submit:
|
||||||
qa.submit()
|
qa.submit()
|
||||||
|
|
||||||
return qa
|
return qa
|
||||||
|
@ -302,6 +302,9 @@ def has_duplicate_serial_no(sn, sle):
|
|||||||
if sn.warehouse:
|
if sn.warehouse:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
if sn.company != sle.company:
|
||||||
|
return False
|
||||||
|
|
||||||
status = False
|
status = False
|
||||||
if sn.purchase_document_no:
|
if sn.purchase_document_no:
|
||||||
if sle.voucher_type in ['Purchase Receipt', 'Stock Entry', "Purchase Invoice"] and \
|
if sle.voucher_type in ['Purchase Receipt', 'Stock Entry', "Purchase Invoice"] and \
|
||||||
@ -357,6 +360,7 @@ def auto_make_serial_nos(args):
|
|||||||
sr.warehouse = args.get('warehouse') if args.get('actual_qty', 0) > 0 else None
|
sr.warehouse = args.get('warehouse') if args.get('actual_qty', 0) > 0 else None
|
||||||
sr.batch_no = args.get('batch_no')
|
sr.batch_no = args.get('batch_no')
|
||||||
sr.location = args.get('location')
|
sr.location = args.get('location')
|
||||||
|
sr.company = args.get('company')
|
||||||
if sr.sales_order and args.get('voucher_type') == "Stock Entry" \
|
if sr.sales_order and args.get('voucher_type') == "Stock Entry" \
|
||||||
and not args.get('actual_qty', 0) > 0:
|
and not args.get('actual_qty', 0) > 0:
|
||||||
sr.sales_order = None
|
sr.sales_order = None
|
||||||
|
@ -7,6 +7,12 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe, unittest
|
import frappe, unittest
|
||||||
|
|
||||||
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
|
||||||
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||||
|
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||||
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||||
|
|
||||||
test_dependencies = ["Item"]
|
test_dependencies = ["Item"]
|
||||||
test_records = frappe.get_test_records('Serial No')
|
test_records = frappe.get_test_records('Serial No')
|
||||||
|
|
||||||
@ -29,3 +35,21 @@ class TestSerialNo(unittest.TestCase):
|
|||||||
|
|
||||||
sr.warehouse = "_Test Warehouse - _TC"
|
sr.warehouse = "_Test Warehouse - _TC"
|
||||||
self.assertTrue(SerialNoCannotCannotChangeError, sr.save)
|
self.assertTrue(SerialNoCannotCannotChangeError, sr.save)
|
||||||
|
|
||||||
|
def test_inter_company_transfer(self):
|
||||||
|
se = make_serialized_item(target_warehouse="_Test Warehouse - _TC")
|
||||||
|
serial_nos = get_serial_nos(se.get("items")[0].serial_no)
|
||||||
|
|
||||||
|
create_delivery_note(item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0])
|
||||||
|
|
||||||
|
wh = create_warehouse("_Test Warehouse", company="_Test Company 1")
|
||||||
|
make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0],
|
||||||
|
company="_Test Company 1", warehouse=wh)
|
||||||
|
|
||||||
|
serial_no = frappe.db.get_value("Serial No", serial_nos[0], ["warehouse", "company"], as_dict=1)
|
||||||
|
|
||||||
|
self.assertEqual(serial_no.warehouse, wh)
|
||||||
|
self.assertEqual(serial_no.company, "_Test Company 1")
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
@ -271,26 +271,38 @@ class StockReconciliation(StockController):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_items(warehouse, posting_date, posting_time, company):
|
def get_items(warehouse, posting_date, posting_time, company):
|
||||||
items = frappe.db.sql('''select i.name, i.item_name from `tabItem` i, `tabBin` bin where i.name=bin.item_code
|
lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"])
|
||||||
and i.disabled=0 and bin.warehouse=%s''', (warehouse), as_dict=True)
|
items = frappe.db.sql("""
|
||||||
|
select i.name, i.item_name, bin.warehouse
|
||||||
|
from tabBin bin, tabItem i
|
||||||
|
where i.name=bin.item_code and i.disabled=0
|
||||||
|
and exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=bin.warehouse)
|
||||||
|
""", (lft, rgt))
|
||||||
|
|
||||||
items += frappe.db.sql('''select i.name, i.item_name from `tabItem` i, `tabItem Default` id where i.name = id.parent
|
items += frappe.db.sql("""
|
||||||
and i.is_stock_item=1 and i.has_serial_no=0 and i.has_batch_no=0 and i.has_variants=0 and i.disabled=0
|
select i.name, i.item_name, id.default_warehouse
|
||||||
and id.default_warehouse=%s and id.company=%s group by i.name''', (warehouse, company), as_dict=True)
|
from tabItem i, `tabItem Default` id
|
||||||
|
where i.name = id.parent
|
||||||
|
and exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=id.default_warehouse)
|
||||||
|
and i.is_stock_item = 1 and i.has_serial_no = 0 and i.has_batch_no = 0
|
||||||
|
and i.has_variants = 0 and i.disabled = 0 and id.company=%s
|
||||||
|
group by i.name
|
||||||
|
""", (lft, rgt, company))
|
||||||
|
|
||||||
res = []
|
res = []
|
||||||
for item in items:
|
for d in set(items):
|
||||||
qty, rate = get_stock_balance(item.name, warehouse, posting_date, posting_time,
|
stock_bal = get_stock_balance(d[0], d[2], posting_date, posting_time,
|
||||||
with_valuation_rate=True)
|
with_valuation_rate=True)
|
||||||
|
|
||||||
|
if frappe.db.get_value("Item", d[0], "disabled") == 0:
|
||||||
res.append({
|
res.append({
|
||||||
"item_code": item.name,
|
"item_code": d[0],
|
||||||
"warehouse": warehouse,
|
"warehouse": d[2],
|
||||||
"qty": qty,
|
"qty": stock_bal[0],
|
||||||
"item_name": item.item_name,
|
"item_name": d[1],
|
||||||
"valuation_rate": rate,
|
"valuation_rate": stock_bal[1],
|
||||||
"current_qty": qty,
|
"current_qty": stock_bal[0],
|
||||||
"current_valuation_rate": rate
|
"current_valuation_rate": stock_bal[1]
|
||||||
})
|
})
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
@ -10,7 +10,9 @@ from frappe.utils import flt, nowdate, nowtime
|
|||||||
from erpnext.accounts.utils import get_stock_and_account_difference
|
from erpnext.accounts.utils import get_stock_and_account_difference
|
||||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
|
||||||
from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after
|
from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after
|
||||||
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError
|
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError, get_items
|
||||||
|
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||||
|
from erpnext.stock.doctype.item.test_item import create_item
|
||||||
|
|
||||||
class TestStockReconciliation(unittest.TestCase):
|
class TestStockReconciliation(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -79,6 +81,19 @@ class TestStockReconciliation(unittest.TestCase):
|
|||||||
|
|
||||||
set_perpetual_inventory(0)
|
set_perpetual_inventory(0)
|
||||||
|
|
||||||
|
def test_get_items(self):
|
||||||
|
create_warehouse("_Test Warehouse Group 1", {"is_group": 1})
|
||||||
|
create_warehouse("_Test Warehouse Ledger 1",
|
||||||
|
{"is_group": 0, "parent_warehouse": "_Test Warehouse Group 1 - _TC"})
|
||||||
|
|
||||||
|
create_item("_Test Stock Reco Item", is_stock_item=1, valuation_rate=100,
|
||||||
|
warehouse="_Test Warehouse Ledger 1 - _TC", opening_stock=100)
|
||||||
|
|
||||||
|
items = get_items("_Test Warehouse Group 1 - _TC", nowdate(), nowtime(), "_Test Company")
|
||||||
|
|
||||||
|
self.assertEqual(["_Test Stock Reco Item", "_Test Warehouse Ledger 1 - _TC", 100],
|
||||||
|
[items[0]["item_code"], items[0]["warehouse"], items[0]["qty"]])
|
||||||
|
|
||||||
def insert_existing_sle(self):
|
def insert_existing_sle(self):
|
||||||
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
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ from erpnext import set_perpetual_inventory
|
|||||||
from frappe.test_runner import make_test_records
|
from frappe.test_runner import make_test_records
|
||||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account
|
from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account
|
||||||
|
|
||||||
|
import erpnext
|
||||||
import frappe
|
import frappe
|
||||||
import unittest
|
import unittest
|
||||||
test_records = frappe.get_test_records('Warehouse')
|
test_records = frappe.get_test_records('Warehouse')
|
||||||
@ -90,15 +91,24 @@ class TestWarehouse(unittest.TestCase):
|
|||||||
self.assertTrue(frappe.db.get_value("Warehouse",
|
self.assertTrue(frappe.db.get_value("Warehouse",
|
||||||
filters={"account": "Test Warehouse for Merging 2 - _TC"}))
|
filters={"account": "Test Warehouse for Merging 2 - _TC"}))
|
||||||
|
|
||||||
def create_warehouse(warehouse_name):
|
def create_warehouse(warehouse_name, properties=None, company=None):
|
||||||
if not frappe.db.exists("Warehouse", warehouse_name + " - _TC"):
|
if not company:
|
||||||
|
company = "_Test Company"
|
||||||
|
|
||||||
|
warehouse_id = erpnext.encode_company_abbr(warehouse_name, company)
|
||||||
|
if not frappe.db.exists("Warehouse", warehouse_id):
|
||||||
w = frappe.new_doc("Warehouse")
|
w = frappe.new_doc("Warehouse")
|
||||||
w.warehouse_name = warehouse_name
|
w.warehouse_name = warehouse_name
|
||||||
w.parent_warehouse = "_Test Warehouse Group - _TC"
|
w.parent_warehouse = "_Test Warehouse Group - _TC"
|
||||||
w.company = "_Test Company"
|
w.company = company
|
||||||
make_account_for_warehouse(warehouse_name, w)
|
make_account_for_warehouse(warehouse_name, w)
|
||||||
w.account = warehouse_name + " - _TC"
|
w.account = warehouse_id
|
||||||
|
if properties:
|
||||||
|
w.update(properties)
|
||||||
w.save()
|
w.save()
|
||||||
|
return w.name
|
||||||
|
else:
|
||||||
|
return warehouse_id
|
||||||
|
|
||||||
def make_account_for_warehouse(warehouse_name, warehouse_obj):
|
def make_account_for_warehouse(warehouse_name, warehouse_obj):
|
||||||
if not frappe.db.exists("Account", warehouse_name + " - _TC"):
|
if not frappe.db.exists("Account", warehouse_name + " - _TC"):
|
||||||
|
@ -36,6 +36,7 @@ def get_item_details(args):
|
|||||||
"is_subcontracted": "Yes" / "No",
|
"is_subcontracted": "Yes" / "No",
|
||||||
"ignore_pricing_rule": 0/1
|
"ignore_pricing_rule": 0/1
|
||||||
"project": ""
|
"project": ""
|
||||||
|
"set_warehouse": ""
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
args = process_args(args)
|
args = process_args(args)
|
||||||
@ -189,7 +190,6 @@ def get_basic_details(args, item):
|
|||||||
"project": "",
|
"project": "",
|
||||||
barcode: "",
|
barcode: "",
|
||||||
serial_no: "",
|
serial_no: "",
|
||||||
warehouse: "",
|
|
||||||
currency: "",
|
currency: "",
|
||||||
update_stock: "",
|
update_stock: "",
|
||||||
price_list: "",
|
price_list: "",
|
||||||
@ -219,7 +219,7 @@ def get_basic_details(args, item):
|
|||||||
item_defaults = get_item_defaults(item.name, args.company)
|
item_defaults = get_item_defaults(item.name, args.company)
|
||||||
item_group_defaults = get_item_group_defaults(item.name, args.company)
|
item_group_defaults = get_item_group_defaults(item.name, args.company)
|
||||||
|
|
||||||
warehouse = user_default_warehouse or item_defaults.get("default_warehouse") or\
|
warehouse = args.get("set_warehouse") or user_default_warehouse or item_defaults.get("default_warehouse") or\
|
||||||
item_group_defaults.get("default_warehouse") or args.warehouse
|
item_group_defaults.get("default_warehouse") or args.warehouse
|
||||||
|
|
||||||
if args.get('doctype') == "Material Request" and not args.get('material_request_type'):
|
if args.get('doctype') == "Material Request" and not args.get('material_request_type'):
|
||||||
@ -273,7 +273,7 @@ def get_basic_details(args, item):
|
|||||||
"transaction_date": args.get("transaction_date")
|
"transaction_date": args.get("transaction_date")
|
||||||
})
|
})
|
||||||
|
|
||||||
if item.enable_deferred_revenue:
|
if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"):
|
||||||
out.update(calculate_service_end_date(args, item))
|
out.update(calculate_service_end_date(args, item))
|
||||||
|
|
||||||
# calculate conversion factor
|
# calculate conversion factor
|
||||||
@ -310,9 +310,15 @@ def calculate_service_end_date(args, item=None):
|
|||||||
if not item:
|
if not item:
|
||||||
item = frappe.get_cached_doc("Item", args.item_code)
|
item = frappe.get_cached_doc("Item", args.item_code)
|
||||||
|
|
||||||
enable_deferred = "enable_deferred_revenue" if args.doctype=="Sales Invoice" else "enable_deferred_expense"
|
doctype = args.get("parenttype") or args.get("doctype")
|
||||||
no_of_months = "no_of_months" if args.doctype=="Sales Invoice" else "no_of_months_exp"
|
if doctype == "Sales Invoice":
|
||||||
account = "deferred_revenue_account" if args.doctype=="Sales Invoice" else "deferred_expense_account"
|
enable_deferred = "enable_deferred_revenue"
|
||||||
|
no_of_months = "no_of_months"
|
||||||
|
account = "deferred_revenue_account"
|
||||||
|
else:
|
||||||
|
enable_deferred = "enable_deferred_expense"
|
||||||
|
no_of_months = "no_of_months_exp"
|
||||||
|
account = "deferred_expense_account"
|
||||||
|
|
||||||
service_start_date = args.service_start_date if args.service_start_date else args.transaction_date
|
service_start_date = args.service_start_date if args.service_start_date else args.transaction_date
|
||||||
service_end_date = add_months(service_start_date, item.get(no_of_months))
|
service_end_date = add_months(service_start_date, item.get(no_of_months))
|
||||||
@ -336,7 +342,7 @@ def get_default_expense_account(args, item, item_group):
|
|||||||
or args.expense_account)
|
or args.expense_account)
|
||||||
|
|
||||||
def get_default_deferred_account(args, item, fieldname=None):
|
def get_default_deferred_account(args, item, fieldname=None):
|
||||||
if item.enable_deferred_revenue:
|
if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"):
|
||||||
return (item.get(fieldname)
|
return (item.get(fieldname)
|
||||||
or args.get(fieldname)
|
or args.get(fieldname)
|
||||||
or frappe.get_cached_value('Company', args.company, "default_"+fieldname))
|
or frappe.get_cached_value('Company', args.company, "default_"+fieldname))
|
||||||
@ -364,22 +370,24 @@ def get_price_list_rate(args, item_doc, out):
|
|||||||
meta = frappe.get_meta(args.parenttype or args.doctype)
|
meta = frappe.get_meta(args.parenttype or args.doctype)
|
||||||
|
|
||||||
if meta.get_field("currency") or args.get('currency'):
|
if meta.get_field("currency") or args.get('currency'):
|
||||||
|
pl_details = get_price_list_currency_and_exchange_rate(args)
|
||||||
|
args.update(pl_details)
|
||||||
validate_price_list(args)
|
validate_price_list(args)
|
||||||
if meta.get_field("currency") and args.price_list:
|
if meta.get_field("currency") and args.price_list:
|
||||||
validate_conversion_rate(args, meta)
|
validate_conversion_rate(args, meta)
|
||||||
|
|
||||||
price_list_rate = get_price_list_rate_for(args, item_doc.name) or 0
|
price_list_rate = get_price_list_rate_for(args, item_doc.name) or 0
|
||||||
|
|
||||||
|
# variant
|
||||||
|
if not price_list_rate and item_doc.variant_of:
|
||||||
|
price_list_rate = get_price_list_rate_for(args, item_doc.variant_of)
|
||||||
|
|
||||||
# insert in database
|
# insert in database
|
||||||
if not price_list_rate:
|
if not price_list_rate:
|
||||||
if args.price_list and args.rate:
|
if args.price_list and args.rate:
|
||||||
insert_item_price(args)
|
insert_item_price(args)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
# variant
|
|
||||||
if not price_list_rate and item_doc.variant_of:
|
|
||||||
price_list_rate = get_price_list_rate_for(args, item_doc.variant_of)
|
|
||||||
|
|
||||||
out.price_list_rate = flt(price_list_rate) * flt(args.plc_conversion_rate) \
|
out.price_list_rate = flt(price_list_rate) * flt(args.plc_conversion_rate) \
|
||||||
/ flt(args.conversion_rate)
|
/ flt(args.conversion_rate)
|
||||||
|
|
||||||
@ -548,6 +556,7 @@ def validate_conversion_rate(args, meta):
|
|||||||
validate_conversion_rate(args.price_list_currency, args.plc_conversion_rate,
|
validate_conversion_rate(args.price_list_currency, args.plc_conversion_rate,
|
||||||
meta.get_label("plc_conversion_rate"), args.company)
|
meta.get_label("plc_conversion_rate"), args.company)
|
||||||
|
|
||||||
|
if meta.get_field("plc_conversion_rate"):
|
||||||
args.plc_conversion_rate = flt(args.plc_conversion_rate,
|
args.plc_conversion_rate = flt(args.plc_conversion_rate,
|
||||||
get_field_precision(meta.get_field("plc_conversion_rate"),
|
get_field_precision(meta.get_field("plc_conversion_rate"),
|
||||||
frappe._dict({"fields": args})))
|
frappe._dict({"fields": args})))
|
||||||
@ -786,7 +795,7 @@ def get_price_list_uom_dependant(price_list):
|
|||||||
if not result:
|
if not result:
|
||||||
throw(_("Price List {0} is disabled or does not exist").format(price_list))
|
throw(_("Price List {0} is disabled or does not exist").format(price_list))
|
||||||
|
|
||||||
return result.price_not_uom_dependant
|
return not result.price_not_uom_dependant
|
||||||
|
|
||||||
|
|
||||||
def get_price_list_currency_and_exchange_rate(args):
|
def get_price_list_currency_and_exchange_rate(args):
|
||||||
|
46
erpnext/templates/emails/daily_project_summary.html
Normal file
46
erpnext/templates/emails/daily_project_summary.html
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
|
<tr>
|
||||||
|
<h3>{{ title }}</h3>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{% for user in users %}
|
||||||
|
<table class="panel-header" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
|
<tr height="10"></tr>
|
||||||
|
<tr>
|
||||||
|
<td width="15"></td>
|
||||||
|
<td valign="top" width="24">
|
||||||
|
{% if user.image %}
|
||||||
|
<img class="sender-avatar" width="24" height="24" embed="{{ user.image }}"/>
|
||||||
|
{% else %}
|
||||||
|
<div class="sender-avatar-placeholder">
|
||||||
|
{{ user.full_name[0] }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td width="10"></td>
|
||||||
|
<td>
|
||||||
|
<div class="text-medium text-muted">
|
||||||
|
<span>{{ user.full_name }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td width="15"></td>
|
||||||
|
</tr>
|
||||||
|
<tr height="10"></tr>
|
||||||
|
</table>
|
||||||
|
<table class="panel-body" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
|
<tr height="10"></tr>
|
||||||
|
<tr>
|
||||||
|
<td width="15"></td>
|
||||||
|
<td>
|
||||||
|
<div>
|
||||||
|
{{ user.project_status }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td width="15"></td>
|
||||||
|
</tr>
|
||||||
|
<tr height="10"></tr>
|
||||||
|
</table>
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||||
|
<tr height="20"></tr>
|
||||||
|
</table>
|
||||||
|
{% endfor %}
|
Loading…
Reference in New Issue
Block a user